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 locale —
Site.defaultLocale. Used when a request doesn't specify one. - Supported locales —
Site.supportedLocales. Drives the locale switcher in the admin UI and thehreflangalternates in sitemaps. - Fallback chain —
Site.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 underlocale = "__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:
- If the field is non-localizable, read
__shared. - If the field is localizable, build a chain
[locale, ...fallback, site.defaultLocale]and return the first value present. - 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.subdomain—en.example.com/fr.example.com.header—Accept-Languagedrives 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.