Skip to main content

languages

The installation-wide pool of supported BCP-47 language tags. Owns the public i18n/config read path that storefront + admin consume to render their language pickers, plus a small LocaleService that implements the translation-fallback rule.

Public surface​

Verb + PathAudiencePurpose
GET /api/v1/i18n/configstorefront / adminActive languages + currencies + the configured defaults
GET /api/v1/admin/languagesadminFull list, including inactive rows
PUT /api/v1/admin/languages/:codeadminUpsert
POST /api/v1/admin/languages/:code/defaultadminPromote to default (atomically demotes the prior default)
DELETE /api/v1/admin/languages/:codeadminRemove (rejected for the default)

The currency catalogue has the same shape under /api/v1/admin/currencies, and those routes are currencies' — see currencies. They were registered here until 2026-08-29, on catalog:write, serving another module's table for no caller in this repository. What this module still composes is GET /api/v1/i18n/config, which answers with both catalogues and both defaults in one public payload and reads the currency half over currencyReadPort.

The four admin language routes above enforce catalog:write. That is a neighbourhood claim of the same kind and has not been repaired.

Defaults​

Exactly zero or one row in languages has is_default = true, enforced by a partial unique index on (is_default) WHERE is_default = true. Setting a new default runs the demote-then-promote pair inside one MikroORM transaction so the partial unique index is never violated mid-flight.

The LanguageService.setDefault() rejects rows where isActive=false (409 VALIDATION_FAILED), and the remove() path refuses to delete a row that is currently the default.

Bootstrap​

Migration 012 inserts two rows so quickstart works without an admin step:

  • en-US — default, active
  • pl-PL — active

The customer-facing label and symbol (currencies) values are written with Postgres U&'…' Unicode literals so the migration source file stays ASCII-only (engineering artifacts stay English-only and ASCII-only; the runtime row reflects what the storefront should render).

Translation-fallback (LocaleService)​

LocaleService.pickLocalizedValue(record, requestedLocale, defaultLocale?) implements the lookup chain:

  1. requested locale, if present in the record.
  2. configured default locale, if supplied and present.
  3. first present value in the record.
  4. empty string.

resolveRequestLocale(acceptLanguageHeader, activeLocales) parses an Accept-Language header (q-weighted) and returns the highest-priority match from the active language pool, with a language-only fallback so en-GB matches en-US. Falls back to the configured default when nothing matches.

The default-locale lookup is cached for 60 seconds; admin mutations call invalidateDefault() so the cache flushes immediately after a change.

Entities​

Language — natural primary key on the BCP-47 code; label, isDefault, isActive, sortOrder.

Extension points​

  • Per-Sales-Channel default — when a Sales Channel ships its own language, hook the resolver before LocaleService.resolveRequestLocale() and use the channel's default instead of the global one.
  • Translation pull/push — emit a domain event when a localized field changes and let an integration consume it for an external translation workflow.