Skip to content

Localization

Content can carry a value per language, and a page can have a different address per language. This page explains how the Delivery API chooses which value you get and which address a page has.

An organization’s locales form a tree. One locale is the root, the organization’s default language, and every other locale names a parent to fall back to:

[
{ "code": "en", "parentCode": null },
{ "code": "fr", "parentCode": "en" },
{ "code": "fr-CA", "parentCode": "fr" }
]

This is what listLocales returns. The tree is what lets a regional variant fall back to its base language before the default: fr-CA falls back to fr, and fr falls back to en.

An organization with no locales configured has no tree. Every value resolves to its default, and nothing on this page applies.

A field marked Localizable holds a default value plus overrides for particular locales. Any field type can be localizable, including structural ones: a localized image field can hold a different file per locale, and a localized Component field can reference a different Component per locale.

Stored, a localizable value is an envelope:

{ "default": "Our coffees", "locales": { "fr": "Nos cafés" } }

Delivered, it is normally one plain value.

Send one locale code as locale=. Delivery then resolves every localizable value in the response like this:

  1. It looks the code up in the tree. The match ignores case but is otherwise exact: fr-CA is not shortened to fr, and a code the tree does not contain matches nothing.
  2. It walks from the matched locale up through its parents to the root, taking the first locale that has an override.
  3. If none does, it uses the default.

With the tree above, locale=fr-CA tries fr-CA, then fr, then en, then the default. A code that is not in the tree, or no locale= at all, gives the default everywhere.

The Delivery API does not read Accept-Language, since a response must depend only on its URL. Choosing the one code to send is your site’s job. @ebitex/content-sdk does it for you by matching the browser’s preferred languages (each region tag, followed by its base language) against listLocales.

To receive the envelopes instead and resolve them yourself, send raw=true.

A page’s slug is localizable too. A page with the slug coffees and a French override cafes has the path /coffees in the default locale and /cafes in French. Each locale in which some slug is localized has its own set of paths. That set is called a locale’s slot.

Two things about slots are easy to get wrong.

A slot covers the whole site. Once any page has a French slug, every page gets a French path. Pages with no French slug use their default slug in the French slot. So /about exists in French as soon as /cafes does, even if nobody has touched the About page.

A page’s address in a locale does not mean the page is translated. Content does not record whether a page has been translated, and a page is often translated while keeping its slug. The sitemap and navigation report a path for every page in every slot. If your site should advertise only translated pages (in hreflang, for example), deciding which pages count is up to you. @ebitex/content-sdk’s sitemap builder lets you drop the alternates you don’t want.

Once a page has a slug in a locale, its default path is not an address in that locale. If /coffees is /cafes in French, requesting /coffees with locale=fr answers path_not_found. Always build links from the addresses the API returns, never by combining another locale’s path with a different locale=.

If you request a locale whose slot does not exist yet (no page has a slug in it), paths are looked up in the default slot. Content is still resolved in the requested locale.

Each site chooses how an address says which language it is in. The setting is per site and per delivery environment, and it is made in the app, under Configure → Sites.

Strategy Address Locale comes from
invariant (the default) /cafes/ethiopia-guji The locale= parameter
pathPrefix /fr/cafes/ethiopia-guji The first path segment
localeHost https://fr.example.com/cafes/ethiopia-guji The host name

Under pathPrefix, pages in the default locale have no prefix unless the site turns on Prefix the default locale too. Whichever spelling is chosen, the other one redirects, so every page has exactly one address. A top-level page whose slug equals a locale code cannot be reached under this strategy. Under localeHost, each locale has one host, which can be a subdomain or a separate domain, and every locale in the tree needs one.

Under invariant, send the path and locale=.

Under pathPrefix, send the whole address, prefix included, and no locale=:

GET /content/delivery/v1/path/fr/cafes/ethiopia-guji?site=<site id>

Under localeHost, send the path and the host you are serving, and neither site= nor locale=. The host decides both:

GET /content/delivery/v1/path/cafes/ethiopia-guji?host=fr.example.com

Send host= as a query parameter. The Host header of a request to the API names the API, not your site. Sending locale= to a site that takes its locale from the address is refused with 400 locale_not_addressable. On a site with a strategy, the response includes locale, the locale it was resolved in, which is what you need to set <html lang>.

Endpoints that take no address, such as getComponent, listComponents and queryStream, keep taking locale= under every strategy.

Every address the API returns is already written for the site’s strategy: a page’s path, a redirect’s targetPath, a Link’s url, navigation and sitemap paths, and the paths attached to components and stream items. A French page’s links arrive as /fr/… under pathPrefix, and as /… under localeHost, where the host carries the language. Use them as they are. Adding your own prefix produces /fr/fr/….

A link to a page on a different site is written for that site’s strategy, as an absolute URL with its host.

listSites includes each configured site’s addressing, so a client can tell which strategy applies:

{
"rootNodeId": "",
"name": "Northwind Coffee",
"hosts": ["example.com", "fr.example.com"],
"addressing": {
"strategy": "localeHost",
"hosts": [
{ "host": "example.com", "locale": "en" },
{ "host": "fr.example.com", "locale": "fr" }
]
}
}

A site with no addressing member is invariant.

Until a locale has a slot, its requests are answered from the default slot, so /coffees with locale=fr finds the page. When the first French slug is published, the French slot comes into existence. Every page given a French slug in that same publish stops answering at its old French address, and requests for it get path_not_found rather than a redirect. This happens once per locale, under every strategy, because it is about when a slot first exists, not about how addresses are written.