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.
A site can serve its own languages
Section titled “A site can serve its own languages”By default every site serves the organization’s whole tree. A site can instead be customized in the app: it then serves the organization’s default language plus the languages it picks, each falling back to the default or to another language it serves. Two sites in one organization can differ, and a site can serve only the default.
Ask for one site’s languages with site:
GET /content/delivery/v1/locales?site=<site id>&env=<environment id>A site that has not been customized answers the organization’s list. GET /sites also carries a
customized site’s languages as a locales member, default first, so a client that already read
/sites does not need a second request. The member is absent for a site that follows the
organization.
A customized site refuses a locale it does not serve with 400 locale_not_in_site, on every read
that names the site. It never answers with another site’s languages, and it does not fall back to the
default silently. Choose the code to send from the site’s own list, not the organization’s.
@ebitex/content-sdk and the .NET client do this for you, so customize a site only once its
consumers run a version that reads the site’s list.
Under pathPrefix, a leading path segment is read as a locale only when this site serves it. A
segment that is one of the organization’s languages but not this site’s is just part of the path.
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.