Most localization pipelines assume content is static: translate it once at build time, ship string bundles, done. But modern applications are full of content that doesn't exist until a user requests it, dynamic CMS blocks, user-generated text, real-time notifications, chat messages, product feeds that update hourly. When your translation needs outpace your release cycle, you need runtime localization patterns that call a translation API on the fly without wrecking latency, blowing through rate limits, or mangling ICU placeholders.
This guide walks through the integration patterns that let you serve dynamically translated content in production: locale negotiation, intelligent caching, glossary injection, placeholder preservation, and the resilience patterns, retries, circuit breakers, fallback chains, that keep your app responsive when the translation service hiccups.
If your team is evaluating how to build this kind of runtime layer, explore how Ollang's translation API fits into your architecture.
Locale Negotiation Strategies
Before you translate anything, you need to know what language to translate into. Getting this wrong means wasted API calls, cache misses, and a degraded user experience.
Accept-Language Header Parsing and Quality Weighting
The HTTP Accept-Language header remains the most reliable ambient signal for locale preference. A typical header looks like:
Accept-Language: fr-CA, fr;q=0.9, en;q=0.5
The q values (quality weights) express preference order. Your middleware should parse these according to RFC 9110 Β§12.5.4, sort by descending quality, and match against your supported locale set using BCP 47 subtag matching. A request for fr-CA should fall back to fr if you don't support Canadian French specifically.
Key implementation details:
- Normalize tags to lowercase with hyphen separators (fr-CA, not fr_ca).
- Strip unsupported subtags (script, region) gracefully rather than rejecting the request.
- Default explicitly. If no header is present or no match is found, fall back to a configured default locale rather than returning untranslated content.
User Profile Locale Override and Fallback Chains
Authenticated users often have a locale preference stored in their profile. This should override the Accept-Language header, since it represents an explicit choice. The resolution order is typically:
- Query parameter or cookie (useful for previewing translations)
- User profile locale (explicit preference)
- Accept-Language header (browser/device signal)
- GeoIP-based guess (least reliable, use only as a last resort)
- Application default locale
Implement this as a simple middleware function that resolves a single targetLocale early in the request lifecycle and attaches it to the request context. Every downstream translation call reads from that resolved locale, no re-negotiation deeper in the stack.
Caching Architecture for Translated Content
Translation API calls carry latency (often on the order of hundreds of milliseconds) and cost. A well-designed cache layer eliminates the vast majority of redundant calls while ensuring freshness.
Edge KV Stores and CDN-Level Translation Caching
For read-heavy applications, storing translated strings at the edge, using a key-value store like Cloudflare Workers KV, Fastly's KV Store, or AWS CloudFront Functions with DynamoDB, can dramatically reduce round-trip time. The cache key should encode enough context to avoid collisions:
translation:{source_locale}:{target_locale}:{content_hash}
Using a hash of the source content (SHA-256 truncated to 16 hex characters works well) rather than the raw text keeps keys compact and avoids encoding issues. Store the translated output alongside metadata: the timestamp, API version, and glossary version used.
Edge caching is most effective for:
- UI chrome and navigation strings that change infrequently
- Product descriptions and CMS content with predictable update cycles
- Notification templates where the structure is fixed but the locale varies
ETag/If-None-Match for Conditional Translation Requests
When your source content may have changed but often hasn't, conditional requests prevent unnecessary re-translation. The pattern:
- On first translation, compute an ETag from the source content hash and glossary version.
- Store the ETag alongside the cached translation.
- On subsequent requests, your middleware checks the cache, finds an existing translation, and compares the current source content hash against the stored ETag.
- If the ETag matches, serve the cached translation directly.
- If it doesn't match, call the translation API and update the cache.
This is cheaper than always calling the API and cheaper than setting aggressive TTLs that force re-translation of unchanged content.
Stale-While-Revalidate for Low-Latency Responses
For content where slight staleness is acceptable, product listings, blog posts, help articles, the stale-while-revalidate pattern delivers the best latency profile:
- Serve the cached translation immediately, even if the TTL has expired.
- In the background, asynchronously call the translation API to refresh the cache.
- The next request gets the fresh translation.
This pattern works naturally with the Cache-Control: stale-while-revalidate directive if you're serving translations through a CDN, or you can implement it at the application level with a background job queue. The key tradeoff: users may briefly see a slightly outdated translation, but they never wait for the API call.
Pre-Translate vs Just-in-Time Translation
Choosing when to translate is as important as choosing how. The two patterns serve different content profiles.
When to Pre-Translate at Build or Publish Time
Pre-translation is the right default for content that is known before the user requests it:
- Static UI strings and labels
- CMS content published on a schedule
- Email templates
- Legal and compliance text where accuracy review is mandatory
Pre-translation lets you run quality review workflows, apply translation memory, and catch errors before they reach users. It also eliminates runtime latency entirely.
If you'd like to compare pre-translation economics and workflows for your catalog, talk to our team about how Ollang handles both workflows through a single API.
When Just-in-Time Calls Are the Only Option
JIT translation is necessary when content is:
- User-generated (comments, reviews, chat messages)
- Assembled dynamically (personalized dashboards, search results with dynamic snippets)
- Arriving from external systems in real time (webhook payloads, third-party feeds)
- Too voluminous to pre-translate across all supported locales
The cost of JIT is latency and API spend. Mitigate both with the caching patterns described above and by batching multiple strings into a single API call where possible.
Decision Tree: Cache vs Translate Live
Use this decision tree to determine the right pattern for a given content type:
Is the content known before the user requests it?
βββ Yes β Pre-translate at build/publish time.
β Store in locale-specific bundles.
β
βββ No β Is the content likely to be requested again
in the same locale within 24 hours?
βββ Yes β Translate on first request,
β cache aggressively (edge KV + ETag).
β Use stale-while-revalidate.
β
βββ No β Is the content ephemeral
(e.g., one-time notification)?
βββ Yes β Translate JIT, skip caching.
β Batch if multiple strings.
β
βββ No β Translate JIT, cache with
short TTL (1-4 hours).
Monitor cache hit rate.
If youβre deciding where to draw the line between pre-translation and JIT in your stack, get architecture guidance tailored to your traffic and content mix: Review your decision tree with our engineers.
Per-Request Glossary and Terminology Injection
Generic translation often fails on domain-specific terms. "Pod" means something different in Kubernetes documentation than in a coffee catalog. Glossary injection at the API level fixes this without post-editing.
Attaching Glossary Entries to API Calls
Most translation APIs accept a glossary or terminology parameter alongside the source text. The pattern is to maintain a glossary store, keyed by domain, product line, or content type, and attach the relevant entries to each API request:
{
"source_language": "en",
"target_language": "de",
"text": "Deploy the pod to the staging cluster.",
"glossary": [
{ "source": "pod", "target": "Pod" },
{ "source": "staging cluster", "target": "Staging-Cluster" }
]
}
This ensures that "pod" is never translated as "HΓΌlse" (the botanical meaning) in your infrastructure documentation.
Managing Glossary Versions and Cache Invalidation
Glossaries evolve. When a term changes, say, marketing renames a feature, you need to invalidate cached translations that used the old glossary. Include the glossary version in your cache key:
translation:en:de:{content_hash}:glossary_v12
When the glossary version increments, all affected cache entries become misses, triggering fresh translations with the updated terms. This is far more reliable than trying to selectively purge individual entries.
