feat(quick-261016-0fm): admin language follows the stored preference

- principalFrom sets PreferredLocale from metadata admin_locale; personal
  API token principals clear it, so token clients keep the request locale
- the guarded admin group runs locale.from-principal after the backend
  guard, overriding the LocaleResolver or Accept-Language locale
- GET /lang honours a supported ?locale= and reports meta.locales
  (cabana.LangEnvelope, cabana.LangMeta)
- Docker test for the override, refusals and /lang; docs and OpenAPI
This commit is contained in:
Jakub Zych
2026-10-16 10:58:29 +02:00
parent 3d2f9d0cf6
commit f67e8915b8
15 changed files with 216 additions and 39 deletions

View File

@@ -79,6 +79,6 @@ The locale lives on the request context, not in a global. For every route, [surf
## Strings for the admin
The admin SPA receives its strings from the server. `phrasebook.Translator.Bundle` returns every key under a prefix as CLDR plural forms, merged over the fallback chain, and `phrasebook.Translator.Forms` returns one key. `phrasebook.Translator.Locales` lists the locales that define keys under a prefix; `Locales("backend::lang.")` is the set of admin languages an administrator can choose. Start-up fails if an admin (`backend::`) string cannot be expressed as CLDR forms, so a pipe string with a condition the SPA cannot evaluate is caught before any admin sees it.
The admin SPA receives its strings from the server. `phrasebook.Translator.Bundle` returns every key under a prefix as CLDR plural forms, merged over the fallback chain, and `phrasebook.Translator.Forms` returns one key. `phrasebook.Translator.Locales` lists the locales that define keys under a prefix; `Locales("backend::lang.")` is the set of admin languages an administrator can choose. The admin language follows the administrator's own preference first: the language chosen in the SPA's user menu is stored per administrator and, on every admin API route behind the guard, replaces the request locale (whatever `Accept-Language` or a `surf.LocaleResolver` picked) through `locale.from-principal`. Without a preference, and always for requests made with a personal API token, the request locale applies. `GET <prefix>/api/v1/lang` takes a `locale` query parameter naming one of those languages, ignores any other value, and lists them in `meta.locales`. Start-up fails if an admin (`backend::`) string cannot be expressed as CLDR forms, so a pipe string with a condition the SPA cannot evaluate is caught before any admin sees it.
The framework ships its validation messages (`lagoon::validate` for `lagoon.Validate`, and the full WinterCMS validator catalog `lagoon::validation` for `lagoon.ValidateRequest`) and admin strings (`backend::lang`) in English and Polish.