diff --git a/AGENTS.md b/AGENTS.md index b972e4a..98a8525 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,6 +127,11 @@ 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. - **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 diff --git a/README.md b/README.md index d996dec..bfd06d3 100644 --- a/README.md +++ b/README.md @@ -940,7 +940,10 @@ is stored, so a link is shareable and a page is what its address says it is. Whe for a language, the host carries `?locale=` onto every link *it* renders (menu, sign-in, its own redirects) and `ctx.localeHref(href)` does the same for a plugin's links. The picker in the sidebar footer (and on the auth pages) lists every installed locale, each a plain link to the -same page in that language — it renders only when more than one is installed. +same page in that language. It renders only when more than one is installed, and **not at all on a +page rendered in response to a POST** — that URL often answers no GET (following the link would +dead-end), and on a re-rendered form it would throw away what the visitor typed. A plugin building +its own picker from `ctx.locales` should do the same (`ctx.req.method`). **Writing a catalog.** `en-US.ts` exports the object and its type; every other locale is written against that type, so a missing or misspelled key is a type error before the app ever boots: diff --git a/e2e-tests/full-flow.spec.ts b/e2e-tests/full-flow.spec.ts index 45efc09..9e4745d 100644 --- a/e2e-tests/full-flow.spec.ts +++ b/e2e-tests/full-flow.spec.ts @@ -88,6 +88,7 @@ test.describe.serial("authenticated admin journey", () => { const row = page.locator("tr", { hasText: `lang-${suffix}@plainpages.local` }); const editHref = await row.locator('a[href^="/admin/users/"]').first().getAttribute("href"); await page.goto(`${editHref}`); + await expect(page.locator('summary[aria-label="Språk"]')).toHaveCount(1); // control: it IS there on the GET await page.getByRole("button", { name: "Skapa återställningskod" }).click(); // POST-only route await expect(page.getByText("Återställningskod skapad")).toBeVisible(); await expect(page.locator('summary[aria-label="Språk"]')).toHaveCount(0); // no dead-end link offered diff --git a/examples/plugins/admin/i18n/en-US.ts b/examples/plugins/admin/i18n/en-US.ts index ede0f3c..ab65993 100644 --- a/examples/plugins/admin/i18n/en-US.ts +++ b/examples/plugins/admin/i18n/en-US.ts @@ -138,8 +138,8 @@ const messages = { "admin.users.new": "New user", "admin.users.pagination": "Users pagination", "admin.users.reactivate": "Reactivate", - "admin.users.recovery.body": - "Give it to the user — they enter it on the password-reset screen to set a new password (generate a fresh one if it has expired).", + "admin.users.recovery.body": "Give it to the user — they enter it to set a new password (generate a fresh one if it has expired):", + "admin.users.recovery.link": "the password-reset screen", "admin.users.recovery.generate": "Generate recovery code", "admin.users.recovery.title": "Recovery code generated", "admin.users.save": "Save changes", diff --git a/examples/plugins/admin/i18n/sv-SE.ts b/examples/plugins/admin/i18n/sv-SE.ts index 349e819..350cf75 100644 --- a/examples/plugins/admin/i18n/sv-SE.ts +++ b/examples/plugins/admin/i18n/sv-SE.ts @@ -138,8 +138,8 @@ const messages: AdminMessages = { "admin.users.new": "Ny användare", "admin.users.pagination": "Sidnavigering för användare", "admin.users.reactivate": "Aktivera igen", - "admin.users.recovery.body": - "Ge den till användaren — koden anges på sidan för lösenordsåterställning för att sätta ett nytt lösenord (skapa en ny om den hunnit gå ut).", + "admin.users.recovery.body": "Ge den till användaren — koden anges för att sätta ett nytt lösenord (skapa en ny om den hunnit gå ut):", + "admin.users.recovery.link": "sidan för lösenordsåterställning", "admin.users.recovery.generate": "Skapa återställningskod", "admin.users.recovery.title": "Återställningskod skapad", "admin.users.save": "Spara ändringar", diff --git a/examples/plugins/admin/views/partials/user-form-body.ejs b/examples/plugins/admin/views/partials/user-form-body.ejs index 1de94a7..e9f2971 100644 --- a/examples/plugins/admin/views/partials/user-form-body.ejs +++ b/examples/plugins/admin/views/partials/user-form-body.ejs @@ -14,7 +14,7 @@ <%- include("partials/alert", { text: locals.error, tone: "neg" }) %> <% } -%> <% if (recovery) { -%> -
<%= recovery.code %><% } %><%= recovery.code %><% } %>