Show the language picker on every page, targeting the nearest page that answers GET
CI / full-gate (push) Successful in 2m39s

This commit is contained in:
2026-08-04 09:37:03 +02:00
parent 7e4c6940c9
commit 37b88b2fe6
7 changed files with 93 additions and 27 deletions
+8 -5
View File
@@ -127,11 +127,14 @@ them. Revisit only if the stated reason stops holding.
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.
- **The language picker never appears on a POST-rendered page.** That URL frequently answers no GET
(`POST /admin/users/:id/recovery` renders a page and has no GET sibling), so a link there dead-ends
on a 405; on a re-rendered form it would also discard the visitor's input. `i18nLocals` returns an
empty `localeSwitch` for any non-GET/HEAD method, and the picker renders nothing below two choices.
Decided 2026-08-03 after a review reproduced the 405.
- **The language picker is on every page, POST-rendered ones included.** Maintainer's call
2026-08-04, overriding an earlier decision to hide it there. The problem it was hiding is real: a
POST-rendered URL frequently answers no GET (`POST /admin/users/:id/recovery`), so a link back to
it dead-ends on a 405. The host therefore resolves the picker's target (`app.ts``switchBase`):
this path when it answers GET, else the same-origin Referer, else `/`. Accepted cost: switching
language on such a page leaves that POST's own result behind (a re-rendered form's input, or a
one-time recovery code). Valid while the picker is expected on literally every page — if that ever
softens, hiding it after a POST is the simpler answer.
- **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