Skip to main content

Localization

Krios localizes per-field, not per-entry. One entry can have its title in three locales, its slug in one, and its publishedAt shared across all. Locale-aware publishing means an English page can be live while its French translation is still draft.

Vocabulary

  • Locale code — BCP 47 string (en-US, fr-CA, zh-Hans-CN).
  • __shared — synthetic locale used to store non-localizable field values. One row per (entry, field, "__shared"); no per-locale overrides.
  • Site default localeSite.defaultLocale. Used when a request doesn't specify one.
  • Supported localesSite.supportedLocales. Drives the locale switcher in the admin UI and the hreflang alternates in sitemaps.
  • Fallback chainSite.fallbackChain (Json). When a locale is missing for a field, the resolver walks this chain in order.

LocaleDefinition table

Tenant-scoped registry. Holds display names + RTL flag:

{ code: "fr-CA", displayName: "French (Canada)", direction: "ltr" }

Used by the admin UI to render locale picker labels and by sitemaps for direction hints.

Per-field localizable flag

{ "apiName": "title", "fieldType": "text", "isLocalizable": true }
  • isLocalizable: true — the field has a value per locale.
  • isLocalizable: false — one shared value across every locale. Stored under locale = "__shared".

The admin UI hides the localizable toggle for field types where it makes no sense (boolean, slug — slugs are inherently per-site, not per-locale-per-site for routing simplicity).

Resolution

resolveValuesForLocale(db, entry, locale, fields, site) returns the right value for each field:

  1. If the field is non-localizable, read __shared.
  2. If the field is localizable, build a chain [locale, ...fallback, site.defaultLocale] and return the first value present.
  3. If nothing in the chain has a value, return null. Required-field validation happens at publish time, not at read time.

Publishing per locale

ContentLocaleState holds the published flag per (entry, locale). Publishing one locale doesn't publish the others.

POST /api/v1/projects/{slug}/entries/{id}/publish
{ "locale": "fr-CA" }

If the entry has no fr-CA field values yet, the publish 422s with the missing-required-fields list.

Locale-aware slugs

Slug fields are not localizable — one slug per site. The URL prefix in front of the slug differs per locale via Site.localeResolution:

  • prefix — URLs become /en-US/about / /fr-CA/a-propos.
  • subdomainen.example.com / fr.example.com.
  • headerAccept-Language drives lookup; URLs are bare.

Translation status

Per-locale translation progress is tracked in a parallel TranslationStatus table (introduced in V2): not-started → in-progress → in-review → approved → published, with stale detection. See Translations.

Sitemaps

Sitemap endpoint emits <xhtml:link rel="alternate" hreflang="..."> for every supported locale that has a published route for the entry. See /api/delivery/projects/{slug}/sites/{siteSlug}/sitemap.

API parameters

Every read endpoint accepts ?locale= (defaults to the site default):

GET /api/delivery/projects/demo/sites/main/entries/{id}?locale=fr-CA

Mutations carry locale in the body:

PUT /api/v1/projects/demo/entries/{id}
{ "version": 2, "locale": "fr-CA", "fields": { "title": "Bonjour" } }

Worked example — adding a French translation

POST /api/v1/projects/demo/entries
{ "contentTypeApiName": "blogPost", "locale": "en-US",
"treeParentId": "node_blog", "siteId": "site_main",
"slug": "hello-world",
"fields": { "title": "Hello World", "body": {…} } }

PUT /api/v1/projects/demo/entries/{id}
{ "version": 1, "locale": "fr-CA",
"fields": { "title": "Bonjour", "body": {…} } }

POST /api/v1/projects/demo/entries/{id}/publish { "locale": "en-US" }
POST /api/v1/projects/demo/entries/{id}/publish { "locale": "fr-CA" }

The entry's slug is shared (not localizable), but the URL prefix or subdomain pattern from Site.localeResolution adds the locale context at delivery time.