Most localization workflows break the moment a content team publishes faster than translators can keep up. An editor updates a landing page in the CMS, a developer ships new UI strings in a feature branch, and within hours the production site serves stale or missing translations to half its audience. The root cause is almost always the same: localization is bolted on as an afterthought instead of wired into the content and deployment pipeline from the start.
An API-first approach treats translation as a first-class event in your content lifecycle. CMS webhooks trigger translation requests the instant content changes. CI/CD pipelines gate deployments on translation completeness. CDN caches invalidate per locale when fresh content arrives. This article walks through the architecture, patterns, and security considerations you need to build that system, and keep it running at scale.
If your team is already feeling the pain of disconnected localization workflows, See how Ollang plugs directly into your pipeline.
Event-Driven Content Pipelines with CMS Webhooks
Listening for Publish and Update Events
Every modern headless CMS, Contentful, Strapi, Sanity, Hygraph, and others, supports webhook notifications on content lifecycle events. The two events that matter most for localization are publish (new content goes live for the first time) and update (existing published content changes). Your localization middleware should subscribe to both.
A typical webhook payload from a headless CMS looks something like this:
{
"event": "entry.publish",
"model": "landing_page",
"entry": {
"id": "page_4f92a",
"locale": "en-US",
"fields": {
"title": "Summer Sale, Up to 40% Off",
"body": "<p>Shop our <a href='/collection/summer'>summer collection</a> before it's gone.</p>",
"hero_image": { "ref": "asset_img_8821" },
"seo_description": "Limited-time summer deals on all categories."
},
"version": 14,
"updated_at": "2025-01-15T09:32:00Z"
}
}
Your webhook receiver should immediately acknowledge the event with an HTTP 200 or 202 response, then enqueue the payload for asynchronous processing. Never perform translation API calls synchronously inside the webhook handler, CMS platforms typically enforce response time limits (often five to ten seconds), and a timeout will trigger retries that create duplicate work.
Designing Idempotent Pull/Push Flows
Idempotency is non-negotiable in event-driven localization. Webhooks can fire more than once for the same event due to network retries, CMS bugs, or infrastructure hiccups. Your system must handle duplicate deliveries gracefully.
The simplest approach is to derive a deterministic job key from the content entry ID and its version number (e.g., page_4f92a:v14). Before enqueuing a translation request, check whether a job with that key already exists. If it does, skip or merge the request.
The pull/push flow works like this:
1. Pull: On receiving a webhook, fetch the full entry from the CMS API (webhooks sometimes carry partial payloads or stale snapshots). Use the CMS read API as the source of truth.
2. Diff: Compare the fetched content against the last version you sent for translation. Only submit changed fields to avoid retranslating unchanged segments.
3. Push: Send the changed fields to the translation API. Tag the request with the entry ID, version, source locale, and target locales.
4. Callback: When translations complete, push them back to the CMS via its write API, targeting each locale's entry variant.
This pattern keeps your middleware stateless aside from a lightweight job ledger, and it guarantees that every translation request maps to exactly one content version.
Batching, Retries, and Exponential Backoff
Sending one API call per field per locale per entry will quickly exhaust rate limits on both the CMS and translation sides. Batch related fields into a single translation request whenever the API supports it. Group by entry, not by field.
For retries, implement exponential backoff with jitter. A practical schedule:
| Attempt | Base Delay | With Jitter (example) |
|---|---|---|
| 1 | 1 s | 0.8-1.2 s |
| 2 | 2 s | 1.6-2.4 s |
| 3 | 4 s | 3.2-4.8 s |
| 4 | 8 s | 6.4-9.6 s |
| 5 | 16 s | 12.8-19.2 s |
After five failed attempts, route the job to a dead-letter queue for manual inspection. Never retry indefinitely, unbounded retries mask upstream failures and can amplify outages.
Handling Rich Text, Structured Fields, and References
Parsing Rich Text with Embedded Assets
Rich text fields are the hardest part of CMS localization. A body field might contain inline links, embedded images with alt text, video components, and custom blocks, all serialized as structured JSON (like Contentful's Rich Text AST) or as HTML.
The key rules:
- Extract translatable strings only. Strip structural markup, preserve it as skeleton, and send only the text nodes and translatable attributes (alt text, title attributes, link labels) to translation.
- Preserve placeholders for embedded assets. Replace inline asset references with numbered tokens (e.g., {{asset_1}}) before translation. Reinsert them afterward. This prevents translators from accidentally modifying asset IDs.
- Handle inline formatting. Bold, italic, and link tags should travel with the text segment they wrap. Most translation APIs support inline markup in XLIFF or HTML-like segments.
A field mapping table helps your team stay consistent:
- Short text/title: translatable; send as plain string
- Rich text body: translatable; extract text nodes, preserve structure skeleton
- Image alt text: translatable; extract, translate, write back to asset metadata
- Slug/URL path: sometimes translatable; translate only if locale-specific URLs are used
- Reference/relation: not translatable; resolve, but do not translate the reference ID
- Number/boolean: skip
- SEO meta description: translatable; send as plain string and flag character limits
Reference Resolution and Nested Content
Entries in a headless CMS frequently reference other entries, a landing page links to a product card, which links to a category. Your middleware needs to decide how deep to resolve references for translation.
A practical rule: resolve one level deep. If a landing page references a product card, check whether the product card itself has already been translated. If not, enqueue it as a separate translation job. Do not flatten deeply nested content into a single massive payload, it creates brittle coupling and makes partial updates painful.
Track reference relationships in your job ledger so you can detect when a parent entry is waiting on a child translation to complete before its own locale variant is fully publishable.
Locale Fallbacks, Versioning, and Translation State
Configuring Locale Fallback Chains
Not every locale needs a full translation on day one. A well-designed fallback chain ensures users always see something coherent:
- fr-CA β fr-FR β en-US
- pt-BR β pt-PT β en-US
- zh-HK β zh-TW β en-US
Implement fallbacks at the application layer, not inside the CMS (where fallback behavior varies widely between platforms). When your rendering layer requests content for fr-CA and finds a field untranslated, it walks the chain until it finds a populated value. Log every fallback hit, a high fallback rate for a given locale signals that translations are lagging behind content velocity.
Mapping Translation States to CMS Workflow
Translation jobs move through states that should map cleanly to your CMS's content workflow:
| Translation State | CMS Status Equivalent | Visible to Users? |
|---|---|---|
| Pending | Draft (locale) | No |
| In Progress | Draft (locale) | No |
| Review | In Review (locale) | Preview only |
| Approved | Ready to Publish | No (until deployed) |
| Published | Published (locale) | Yes |
Write translations back to the CMS as drafts. Only promote them to published status after review approval. This prevents half-finished translations from leaking to production.
Version every translation against the source content version. If the English entry is at version 14 and the French translation was produced against version 12, your system should flag it as stale and re-enqueue the changed segments.
CI/CD Integration for Localized Deployments
Pre-Deploy String Extraction and Translation Gating
For UI strings that live in code (button labels, error messages, form placeholders), extract them during the build step rather than relying on manual handoffs. Tools like i18next-parser, formatjs extract, or custom AST scripts can scan your codebase and produce structured locale files (JSON, YAML, or XLIFF).
Wire this extraction into your CI pipeline:
# Example GitHub Actions step
- name: Extract UI strings
run: npx formatjs extract 'src/**/*.tsx' --out-file locale/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'
- name: Push strings to translation API
run: node scripts/push-to-translation.js --source locale/en.json --targets fr,de,ja,es
- name: Gate on translation completeness
run: node scripts/check-translation-status.js --threshold 100 --locales fr,de,ja,es
The gating step is critical. If any target locale falls below your completeness threshold, the pipeline should either block the deploy or deploy with an explicit fallback-to-source-language flag. Teams commonly set a 100% gate for tier-one markets and a lower threshold (such as 95%) for secondary locales, with fallback rendering for the missing strings.
Preview Environments for Linguistic Review
Reviewers need to see translations in context, not in spreadsheets. Spin up per-locale preview environments as part of your pull request workflow. When a translation job completes, deploy a preview build that renders the target locale with the new strings and CMS content.
Pass the preview URL to reviewers automatically, via Slack notification, email, or a comment on the translation job. Include a direct link to the specific page or component that changed. Context-blind review is the leading cause of post-launch localization bugs.
If your team wants previews connected directly to your translation workflow, See how Ollang integrates translations with CI/CD and preview environments.
