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.
Locales and the locale tree
Section titled “Locales and the locale tree”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.
Localizable fields
Section titled “Localizable fields”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.
How delivery picks a value
Section titled “How delivery picks a value”Send one locale code as locale=. Delivery then resolves every localizable value in the response
like this:
- It looks the code up in the tree. The match ignores case but is otherwise exact:
fr-CAis not shortened tofr, and a code the tree does not contain matches nothing. - It walks from the matched locale up through its parents to the root, taking the first locale that has an override.
- 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.
Localized slugs and paths
Section titled “Localized slugs and paths”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.
Locale addressing
Section titled “Locale addressing”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.
Requesting a page
Section titled “Requesting a page”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.comSend 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.
Addresses in responses
Section titled “Addresses in responses”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.
A one-time “not found”
Section titled “A one-time “not found””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.