Use this document to understand how locales move through Contentful, contentful.js, the Experience
API, and the Optimization SDK Suite. It explains which layer owns each locale decision, why the SDKs
resolve locales before fetching Contentful entries, and where applications must keep content, event,
and profile localization aligned.
For entry replacement mechanics, see Entry personalization and variant resolution. For package setup, use the relevant integration guide or package README.
Locale handling in an optimized application has multiple channels. Keeping those channels separate prevents the most common integration bugs.
| Channel | Owned by | Used for |
|---|---|---|
| Contentful locale | Contentful and the application Content Delivery API call | Selecting the localized entry, asset, and linked-entry field values returned by Contentful. |
| SDK-resolved Contentful locale | Optimization SDK locale helpers | Choosing a configured Contentful locale code before an app-owned CDA fetch. |
| Runtime or request locale candidates | Browser, device, route, server request, or application state | Inputs that the SDK can match against configured Contentful locale codes. |
| Experience API request locale | Experience API request option, stateful api.locale, or the SDK-resolved locale |
Localizing Experience API response data such as profile location fields that merge tags can render. |
| Event context locale | Event payload context | Recording the visitor or request locale as analytics and audience-rule data. |
| Application UI or route locale | Application router, i18n framework, or native app state | Choosing URLs, UI strings, navigation, and refetch timing. |
The SDKs can help resolve and expose a Contentful locale, but they do not own routing, translation resources, Contentful fetching, or when the application refreshes data after a language change.
Contentful locales are configured at the environment level. Each locale has a code, one default locale exists for the environment, and non-default locales can have a fallback locale. Contentful can also control whether a locale is included in Content Delivery API and Content Preview API responses.
Contentful supports several localization modeling approaches, including field-level localization, entry-level localization, content type-level localization, and space-level localization. The Optimization SDK Suite does not choose one of those approaches for you. The SDKs care about the delivery payload passed to entry resolution: it must represent one localized view of the baseline entry and its linked optimization entries.
The Content Delivery API (CDA) can return localized entry fields in two important shapes:
| CDA request | Field shape | sys.locale |
SDK entry-resolution compatibility |
|---|---|---|---|
No locale query parameter |
Direct field values from the space default locale | Present | Compatible when the app intentionally uses the default locale. |
locale=<code> |
Direct field values for the requested locale, with Contentful locale fallback applied to missing fields | Present | Compatible. This is the recommended shape for localized optimized entries. |
locale=* |
Locale-keyed field maps such as fields.title['en-US'] |
Not set | Not compatible with SDK entry resolution. |
For a configured locale request such as locale=de-DE, Contentful applies the locale fallback chain
defined in the space when a localized field value is missing. That fallback belongs to Contentful
and happens before the Optimization SDK sees the entry payload.
For locale=*, Contentful returns every available locale under locale-code keys. That shape is
useful for some synchronization and authoring workflows, but it does not match the direct field
values the Optimization SDK resolver reads.
contentful.js locale modifierscontentful.js follows the same delivery shapes:
client.withAllLocales returns entries and assets with all locales.withAllLocales is accepted but not meaningful for
changing that Sync API behavior.The Optimization SDK helper withOptimizationLocale(contentfulClient) is designed for the
single-locale getEntry() and getEntries() path. It injects the live SDK locale when the caller
does not provide a locale query value. It does not convert an all-locale client or Sync API
payload back into the single-locale shape.
Entry personalization joins two independent data sets:
The local resolver then reads fields such as fields.nt_experiences and fields.nt_variants
directly from that Contentful payload. If those fields are locale-keyed maps, the resolver cannot
know which locale branch the application intends to render. Resolving one Contentful locale before
the CDA request keeps the resolver deterministic, typed, and independent of application routing.
SDK-assisted locale resolution also avoids using raw browser, device, or request locales directly as
CDA query values. Runtime locales can differ from Contentful locale codes in casing, separators,
script subtags, market variants, or availability. A browser might report de-AT while the
Contentful space supports only de-DE. The SDK matches runtime input to the configured Contentful
locale list and returns the configured code that the CDA understands.
The SDKs preserve configured Contentful locale codes in their outputs. Matching is case-insensitive,
but a configured value such as en-US remains en-US when exposed as optimization.locale,
client.locale, or contentfulLocale.
SDK locale configuration centers on contentfulLocales and an optional initial app/content
locale:
const optimization = new ContentfulOptimization({
clientId,
environment,
contentfulLocales: {
default: 'en-US',
supported: ['en-US', 'de-DE', 'fr-FR'],
},
locale: 'de-AT',
})
Native SDKs expose the same model with platform-specific syntax:
let config = OptimizationConfig(
clientId: clientId,
contentfulLocales: ContentfulLocales(
default: "en-US",
supported: ["en-US", "de-DE", "fr-FR"]
),
locale: "de-AT"
)
val config = OptimizationConfig(
clientId = clientId,
contentfulLocales = ContentfulLocales(
default = "en-US",
supported = listOf("en-US", "de-DE", "fr-FR"),
),
locale = "de-AT",
)
Copy default and supported from Contentful locale settings or the locale list returned by the
Content Management API. The SDK does not discover the locale list automatically.
Locale resolution has these common configuration cases:
| Configuration | Resolved Contentful locale | Result |
|---|---|---|
No contentfulLocales and no explicit top-level locale |
undefined |
SDK-assisted CDA locale resolution is disabled. Wrapped CDA clients omit locale and Contentful uses the space default. |
No contentfulLocales with explicit top-level locale |
Normalized explicit locale | The SDK uses the explicit locale as-is after validation. The application is responsible for ensuring the space supports it. |
contentfulLocales.default only |
contentfulLocales.default |
Single-locale apps get a concrete CDA locale and default Experience API locale. |
contentfulLocales.default and supported |
Matched configured locale, or default fallback | Localized apps can match browser, device, route, or request candidates to supported Contentful locale codes. |
Use default-only configuration for an app that always renders one Contentful locale. Add supported
when the app needs to match runtime or request locales to multiple configured Contentful locales.
When contentfulLocales is configured, the SDK resolves candidates in this order:
locale or runtime setLocale() value exists, use it as the only
candidate.supported order.contentfulLocales.default when no candidate matches.Exact matches across all candidates win before language-level fallback. For example, with
supported: ['en-US', 'de-DE', 'fr-FR'] and request candidates ['fr-CA', 'de-DE'], the SDK
returns de-DE because it is an exact configured match. It does not return fr-FR merely because
fr-CA appears first.
The order of supported matters for broad fallback. If the runtime candidate is de-AT and the
space supports both de-DE and de-CH, the first matching de-* locale in supported wins.
The SDK normalizes locale candidates by trimming whitespace and converting underscores to hyphens before matching. Matching ignores case, but resolved values preserve configured Contentful locale code casing.
Ambient runtime candidates such as browser languages, device languages, and Accept-Language header
values are ignored when they are not valid locale strings. Explicit locale inputs are stricter:
contentfulLocales.default and contentfulLocales.supported must be valid locale strings.locale must be a valid locale string when present.setLocale(locale) must receive a valid locale string.locale values passed through withOptimizationLocale() must be valid locale
strings.api.locale must be a valid locale string when present.The wildcard value * is intentionally not a valid explicit SDK locale. Use a concrete locale for
entries that feed Optimization SDK entry resolution.
Each SDK uses the same matching rules, but the candidate source differs by runtime.
The Web SDK and React Web SDK are stateful browser runtimes. They collect locale candidates from
navigator.languages and navigator.language unless the application provides an explicit top-level
locale.
The resolved locale is exposed through optimization.locale and optimization.states.locale.
withOptimizationLocale(contentfulClient) injects that locale into getEntry() and getEntries()
when the caller does not provide a locale. React Web exposes the same helper through
useOptimization().
Use optimization.setLocale(nextLocale) when application language state changes after
initialization. In React Web provider-owned instances, changing the provider locale prop calls
setLocale(nextLocale) for you. The method updates SDK locale state and the default Experience API
locale when api.locale is not configured. It does not refetch Contentful entries, rerun routing
loaders, or refresh profile data.
For route-based localization, pass the route locale as the explicit locale candidate. Browser
language preferences are a fallback input, not a replacement for application routing policy.
The Node SDK is stateless. It does not store a current request locale between calls. Call
resolveRequestLocale(reqOrAcceptLanguage) for each request.
That method accepts a raw Accept-Language header or a request-like object. It parses quality
weights, ignores invalid candidates, and returns:
const { contentfulLocale, eventLocale } = optimization.resolveRequestLocale(req)
Use the returned values for different purposes:
| Value | Use |
|---|---|
contentfulLocale |
CDA entry fetches and the per-call Experience API { locale } option, when present. |
eventLocale |
Event payload context, for example page({ locale: eventLocale, ... }). |
When contentfulLocales is not configured, contentfulLocale is absent. Omit CDA and Experience
API locale options intentionally and let those APIs use their defaults. Do not substitute
eventLocale as a CDA locale unless your application has separately verified that it is a
configured Contentful locale.
Server-rendered pages must keep the request-scoped locale with the request-scoped profile and optimization data. Cache raw Contentful payloads by locale if needed; don't cache a profile-resolved entry variant as a global response for another locale or visitor.
The React Native SDK is stateful and uses Intl.DateTimeFormat().resolvedOptions().locale as its
runtime locale candidate unless the application provides an explicit locale.
The resolved locale is exposed through optimization.locale and optimization.states.locale.
withOptimizationLocale(contentfulClient) works the same way as in the Web SDK for contentful.js
clients used by the React Native application layer.
In provider-owned instances, changing the provider locale prop calls setLocale(nextLocale) after
initialization. Locale changes update SDK state, but the app must run its normal screen(),
identify(), and Contentful refetch flow when localized data needs to change.
The native SDKs use the shared JavaScript core through a native bridge. Swift and Kotlin expose the same locale model as the JavaScript SDKs.
| Runtime | Candidate source | Resolved locale surface | Contentful fetch responsibility |
|---|---|---|---|
| iOS | Locale.preferredLanguages, unless OptimizationConfig.locale is set |
client.locale |
The app-owned CDA client uses client.locale. |
| Android | LocaleList.getDefault(), unless OptimizationConfig.locale is set |
client.locale |
The app-owned CDA client uses client.locale. |
Native SDKs do not fetch Contentful entries for the app layer. Use client.locale in your CDA
request code before passing entries to OptimizedEntry or personalizeEntry(...).
Runtime language changes use OptimizationClient.setLocale(...). Like the JavaScript SDKs, this
updates SDK locale state and the default Experience API locale when no explicit API override exists.
It does not fetch new Contentful entries for the application.
The Experience API has its own locale query parameter. In the Optimization SDK Suite, that locale
is not a CDA locale switch. It can localize Experience API response values, including profile
location fields used by merge tags such as location.city and location.country.
Stateful SDKs choose the Experience API request locale this way:
api.locale when configured.Stateless Node code passes the Experience API locale per request:
const { contentfulLocale, eventLocale } = optimization.resolveRequestLocale(req)
const requestOptions = contentfulLocale ? { locale: contentfulLocale } : undefined
const data = await optimization.page(
{
locale: eventLocale,
profile: { id: profileId },
properties: { path: req.path },
},
requestOptions,
)
Use the same resolved Contentful locale for CDA and Experience API requests when merge tags must
render profile values in the same language as the entry. Configure api.locale only when the
Experience API response language must intentionally differ from the Contentful content language.
Event context locale is separate. It describes the event context for analytics and audience rules, but it does not choose the Contentful entry locale and does not override the Experience API query parameter.
The entry resolver expects one localized Contentful payload. Fetch the baseline entry with the resolved Contentful locale and enough include depth for linked optimization and variant entries:
const contentful = optimization.withOptimizationLocale(contentfulClient)
const baselineEntry = await contentful.getEntry(entryId, {
include: 10,
})
The resolver works with direct field values such as:
entry.fields.nt_experiences
optimizationEntry.fields.nt_variants
It does not read locale-keyed values such as:
entry.fields.nt_experiences['en-US']
optimizationEntry.fields.nt_variants['en-US']
Contentful locale fallback can affect the fields the resolver sees. If a localized nt_experiences
field is empty and falls back to another configured locale, the SDK resolves against that delivered
fallback value. If Contentful returns no usable optimization links for the requested locale, SDK
entry resolution returns the baseline entry.
The SDKs deliberately leave several locale decisions to the application:
contentfulLocales aligned with the locales configured in the Contentful environment.api.locale must intentionally differ from the Contentful locale.This separation keeps SDK behavior predictable across web, server, React Native, iOS, and Android runtimes without requiring the SDKs to own application routing or Contentful client setup.
contentful.js withAllLocales, raw CDA locale=*, and
Sync API all-locale payloads return locale-keyed field maps. Use one concrete CDA locale for
entries passed to Optimization SDK entry resolution.contentfulLocales is configuration, not discovery - The SDK does not fetch the space locale
list. Update SDK configuration when Contentful locale settings change.supported order affects broad fallback - Exact configured matches win first, but language
fallback chooses the first matching configured locale by supported order.Accept-Language values can be
unsupported by Contentful. The SDK maps them to configured codes when contentfulLocales exists.contentfulLocales is trusted - The SDK validates syntax but cannot
know whether the Contentful space supports that locale.setLocale() does not fetch - Locale changes update SDK state and default Experience API
locale. The app must refetch Contentful content and call Experience methods again when rendered
data needs to change.api.locale is not the CDA locale - It controls the Experience API request locale. If it
differs from the Contentful locale, merge tag profile values can render in a different language
than the entry content.eventLocale is event data - In Node and SSR, use contentfulLocale for CDA and Experience
API request options. Use eventLocale only in event payload context unless the application has
validated it separately.withOptimizationLocale() does not overwrite a caller's
explicit locale query value. It validates that value and injects the SDK locale only when the
query omits locale.nt_experiences or variant fields to another locale.
Verify fallback rules when personalization links differ by market.locale query parameter and locale=* response shape.contentful.js client chain modifiers
explain single-locale and all-locale client behavior.locale option.