diff --git a/AGENTS.md b/AGENTS.md index 8669410..b972e4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,13 +117,25 @@ them. Revisit only if the stated reason stops holding. whole query string and no href wrapper can reach it. Putting the obligation on each call site was tried first and missed five of eight sites inside one commit — including the admin screens. `ctx.localeHref` remains for hrefs a plugin's own markup emits (the admin example's delete links). + **A form's `action` counts as a link** — a POST replaces the URL as completely as a GET submit, so + the sign-out, consent and auth-card forms carry it too; without that, picking a language and then + saving anything drops back to `Accept-Language`. The one round-trip that cannot carry it is the + Kratos sign-in POST, whose action is an absolute off-site URL. Decided 2026-08-03 after an architecture review; a second pass then found breadcrumbs still raw, so: when a link renders from the core chrome, it is the chrome's job to carry the locale. - **`locale` is a host-owned query param.** It is in `parseListQuery`'s reserved set (`list-query.ts`), so a localized list page doesn't hand a plugin a phantom `locale` filter; the i18n view locals (`t`, `locale`, `locales`, `localeHref`, `localeParam`, `localeSwitch`, `dir`) are likewise reserved names, merged after a handler's `data` so a collision loses the key instead of breaking the shell. -- **`locales/` at the repo root is a drop-in mount, like `plugins/` and `config/`.** A catalog there +- **A plugin-owned render always runs on that plugin's context.** The landing slots (`home`, + `dashboard`) and an `onRequest` short-circuit dispatch a plugin's handler, so they build the + context with `contextFor(pluginId)` exactly as a plugin route does — otherwise `ctx.t` is the core + translator and the plugin's own keys render as bare keys on the pages it owns. Found by review + 2026-08-03 after all three paths shipped with the host's context. +- **`locales/` at the repo root is a drop-in mount, like `plugins/` and `config/`.** The SHIPPED + `en-US` stays the parity baseline even when the mount replaces it, so a mounted catalog is checked + rather than trusted (a mounted `en-US` compared only against itself would boot green with the + whole UI rendering keys). A catalog there for a new tag adds a language; one for a tag the image ships replaces that catalog wholesale, held to the same parity check. Adding a language must not require forking the image. - **An unknown translation key renders as itself.** That single rule is what lets a nav label, diff --git a/README.md b/README.md index 7ae4dd1..d996dec 100644 --- a/README.md +++ b/README.md @@ -1001,8 +1001,10 @@ Three rules worth knowing: - **The core building blocks carry the locale for you** — every href they render (menu, breadcrumbs, pagination, sort headers, row actions, the auth card's links) goes through `localeHref`, and their GET forms carry it as a hidden field, since a GET submit replaces the whole query string. - `ctx.localeHref` is for hrefs your own markup emits, and `localeParam` (a view local: the tag, or - null) for your own GET forms. `locale` is reserved: `parseListQuery` never returns it as a filter. + `ctx.localeHref` is for hrefs and form actions your own markup emits (a POST replaces the URL just + as a GET submit does), and `localeParam` (a view local: the tag, or null) for your own GET forms. + `locale` is reserved: `parseListQuery` never returns it as a filter. Responses carry + `Vary: Accept-Language`, so a cache in front of the app keys on the language too. - **Reuse the core words.** Generic UI verbs live in the core catalog — `common.add/cancel/delete/ edit/new/remove/save`, `filter.*`, `pagination.*`, `table.*` — and a plugin's lookup falls through to them. Keep your catalog for your domain words, so N plugins don't re-translate "Cancel" N times. diff --git a/examples/plugins/admin/views/partials/client-form-body.ejs b/examples/plugins/admin/views/partials/client-form-body.ejs index 63eae55..e55be3c 100644 --- a/examples/plugins/admin/views/partials/client-form-body.ejs +++ b/examples/plugins/admin/views/partials/client-form-body.ejs @@ -10,7 +10,7 @@ <% if (locals.error) { -%> <%- include("partials/alert", { text: locals.error, tone: "neg" }) %> <% } -%> -