Multilingual for headless CMS.
Polylang gave WordPress sites a complete multilingual workflow. PolyLingo brings that workflow to Sanity, Contentful, Webflow, Framer, and every other headless CMS — through a REST API that integrates in an afternoon.
Headless CMS multilingual is an unsolved problem.
Sanity has the internationalization plugin. Contentful has locales. But neither of them translates your content — they just store it in multiple languages. Filling those language slots is still a manual process. Export the English content, run it through a translation tool that breaks your rich text or JSON structure, fix the output, import it back, repeat for every language. For an active content team publishing regularly, this workflow doesn't scale. It also doesn't exist for most smaller setups, which means the content just never gets translated at all.
The headless CMS architecture separates content management from content delivery. This is good for flexibility. But it creates a gap: the CMS stores language variants, but nothing fills those language variants with translated content. You have to build that layer yourself.
Most teams end up in one of two situations: they manually translate content by copying it into DeepL and pasting it back (slow, error-prone, doesn't scale), or they write a custom integration with a translation API that they have to maintain indefinitely. Neither is a good answer. PolyLingo is a clean third option.
PolyLingo is the translation layer your CMS is missing.
PolyLingo integrates directly with your CMS publish workflow. Set up a webhook that fires when content is published, pass the content to PolyLingo, receive translated versions for every language, write them back to your CMS. For Sanity, this is a few lines in a server action. For Contentful, it's a webhook handler. For custom setups, it's an HTTP call. The translation model understands your content format — Markdown, HTML, JSON, rich text — and preserves structure throughout.
The pattern is consistent across every CMS: fetch content in your source language, call the PolyLingo API with all target languages, write the translated content back to the CMS via its management API. This runs as a build-time script, a CI job, or a webhook handler — whichever fits your workflow.
PolyLingo handles Markdown, HTML, and plain text, so it works with whatever format your CMS uses for rich content. Structured fields (headings, body, excerpt) can be translated individually to give you granular control over which fields are translated.
Sanity + PolyLingo
Sanity’s Document Internationalization plugin creates linked document variants per locale. The script below fetches the English base documents and creates translated variants for each target language automatically.
Works with the document-level i18n pattern (one document per locale) as well as the field-level pattern (all locales in one document). For the field-level pattern, loop over fields rather than documents.
// scripts/translate-sanity.mjs
// Fetches published posts and translates each to all target languages
import { createClient } from '@sanity/client'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: 'production',
token: process.env.SANITY_TOKEN,
apiVersion: '2024-01-01',
useCdn: false,
})
const posts = await sanity.fetch(`*[_type == "post" && __i18n_lang == "en"]`)
for (const post of posts) {
const response = await fetch('https://api.usepolylingo.com/v1/translate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.POLYLINGO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: post.body_markdown,
format: 'markdown',
targets: ['es', 'fr', 'de', 'ja', 'zh'],
}),
})
const { translations } = await response.json()
for (const [lang, content] of Object.entries(translations)) {
await sanity.create({
_type: 'post',
__i18n_lang: lang,
__i18n_base: { _type: 'reference', _ref: post._id },
title: translations[lang + '_title'] || post.title,
slug: { current: `${post.slug.current}-${lang}` },
body_markdown: content,
})
}
}Contentful + PolyLingo
Contentful stores locale variants as fields on the same entry. The script below uses the Contentful Management API to fetch English entries, translate them, and write the translated content directly to the locale-specific fields — no manual copy-paste required.
Contentful uses BCP 47 locale codes (e.g. es-ES rather than es). Map PolyLingo’s ISO 639-1 codes to your Contentful locale configuration accordingly.
// scripts/translate-contentful.mjs
// Translates Contentful entries to all target locales
import contentful from 'contentful-management'
const client = contentful.createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
})
const space = await client.getSpace(process.env.CONTENTFUL_SPACE_ID)
const env = await space.getEnvironment('master')
const entries = await env.getEntries({ content_type: 'blogPost', locale: 'en-US' })
for (const entry of entries.items) {
const enBody = entry.fields.body['en-US']
const response = await fetch('https://api.usepolylingo.com/v1/translate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.POLYLINGO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: enBody,
format: 'markdown',
targets: ['es-ES', 'fr-FR', 'de-DE'],
}),
})
const { translations } = await response.json()
for (const [locale, content] of Object.entries(translations)) {
entry.fields.body[locale] = content
}
await entry.update()
await entry.publish()
}Webflow + PolyLingo
Webflow’s Localization API (available on CMS and Business plans) supports locale-specific field content. The script below fetches CMS collection items, translates the HTML body field, and writes translations back to each locale variant via the Webflow v2 API.
Webflow stores rich text fields as HTML. PolyLingo’s HTML translation preserves all Webflow-generated markup — custom classes, attributes, and embedded elements — untouched.
// scripts/translate-webflow.mjs
// Webflow Localization API + PolyLingo
const headers = {
'Authorization': `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
'accept-version': '2.0.0',
'Content-Type': 'application/json',
}
// Fetch English CMS items
const itemsRes = await fetch(
`https://api.webflow.com/v2/collections/${process.env.WEBFLOW_COLLECTION_ID}/items`,
{ headers }
)
const { items } = await itemsRes.json()
for (const item of items) {
const response = await fetch('https://api.usepolylingo.com/v1/translate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.POLYLINGO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: item.fieldData['body-html'],
format: 'html',
targets: ['es', 'fr', 'de'],
}),
})
const { translations } = await response.json()
// Write translated content back to Webflow locale fields
for (const [lang, content] of Object.entries(translations)) {
await fetch(
`https://api.webflow.com/v2/collections/${process.env.WEBFLOW_COLLECTION_ID}/items/${item.id}/locales/${lang}`,
{ method: 'PATCH', headers, body: JSON.stringify({ fieldData: { 'body-html': content } }) }
)
}
}What PolyLingo gives headless CMS users
- ✓Sanity — translate on publish via webhook, write back to document locales
- ✓Contentful — translate entries automatically when English locale is updated
- ✓Webflow — translate CMS collection items via the API
- ✓Any headless CMS with an API — the integration pattern is the same
- ✓Rich text, Markdown, and HTML all preserved correctly
- ✓All 36 languages in one request — no per-language calls
- ✓Works with any CMS that has a management API
- ✓Content can be re-translated on every publish — no manual sync
The standard multilingual CMS workflow
Write content in your source language
Create and publish content in English (or whichever language is your source). Your CMS stores this as the authoritative version. You do not need to change your editorial workflow at all.
Trigger the translation script
Run the script manually, on a schedule, or via a webhook triggered by content publish events in your CMS. The script calls PolyLingo once per document with all target languages, then writes all translations back to your CMS in one pass.
Deploy — translated content is live
Your frontend reads locale-specific content from the CMS as usual. No changes to your frontend code are required. The translated content appears in the correct language for each locale route.
Who this is built for
Content teams on Sanity or Contentful
Your editors publish in English. Translated content appears in all locales automatically, without the editing team needing to interact with translation tools.
Agencies building multilingual sites
Every client site you build needs multilingual support. PolyLingo gives you a reusable, billable integration that works across any headless CMS in your stack.
E-commerce with localised product content
Product descriptions, category pages, and blog content — all translated automatically when published. Combine with locale-specific pricing to deliver a fully localised shopping experience.
Frequently asked questions about headless CMS multilingual
Does PolyLingo work with CMSs not listed here?
Yes. Any CMS with a management API can be integrated using the same pattern — fetch content, call PolyLingo, write back. Prismic, Storyblok, DatoCMS, Strapi, Ghost, and Directus all have management APIs and work with this approach. The integration examples for Sanity, Contentful, and Webflow above illustrate the pattern.
Can I translate rich text with embedded images and links?
Yes. HTML translation preserves all embedded elements including images (src and alt attributes handled correctly), links (href preserved, link text translated), and iframes. The only exception is content explicitly marked as non-translatable — code blocks, for example, are never translated.
How do I handle content that should not be translated?
For structured content with non-translatable fields (slugs, dates, technical identifiers), send only the fields you want translated. For rich text with mixed translatable and non-translatable sections, use the HTML format — PolyLingo will translate text content while preserving code blocks and other structured elements automatically.
What if my CMS has nested content types?
For deeply nested content (documents with references to other documents), translate each document type independently. This avoids circular references and gives you clean control over which content is translated. References between documents are maintained by the CMS — PolyLingo only touches the field content, not document relationships.
How do I keep translations in sync when the source content changes?
The recommended pattern is to trigger the translation script on every publish event via a CMS webhook. This ensures translated content is updated whenever the source changes. For less frequent content updates, running the script on a nightly schedule or before each production deployment works equally well.
Is there a way to mark translations as "needs review" rather than publishing automatically?
This depends on your CMS. Contentful and Sanity both support draft states — you can write translated content as draft rather than published, allowing human review before each locale goes live. The script examples above use publish/create immediately; modify the final step to create drafts instead for a review workflow.
Related guides
Translate HTML without breaking markup
How PolyLingo handles CMS-generated HTML with full tag preservation.
Add multilingual to Next.js
Combine headless CMS content translation with next-intl UI string translation.
Polylang alternative for modern stacks
Migrating multilingual workflow from WordPress to a headless setup.
Add multilingual to your headless CMS today.
Free tier. 100,000 tokens per month. No credit card required.
Free tier — 100,000 tokens per month. Works with any CMS.