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
+6 -5
View File
@@ -8,7 +8,7 @@ const request = (overrides: Partial<I18nRequest> = {}): I18nRequest => ({
locale: "sv-SE",
localeHref: (href) => href,
locales: ["en-US", "sv-SE"],
method: "GET",
switchBase: "/admin/users?q=ada",
t: ENGLISH,
url: new URL("http://localhost/admin/users?q=ada"),
...overrides,
@@ -32,8 +32,9 @@ test("dir follows the locale's script", () => {
assert.equal(i18nLocals(request({ locale: "ar-EG" })).dir, "rtl");
});
test("a page rendered from a POST offers no language links — that URL may have no GET at all", () => {
// Following one would dead-end on a 405 (a POST-only route), or silently discard a re-rendered
// form's input. The picker renders nothing below two choices, so an empty list hides it.
assert.deepEqual(i18nLocals(request({ method: "POST" })).localeSwitch, []);
test("the picker points wherever the host says — after a POST that is the nearest page answering GET", () => {
// The picker is on every page; on a POST-rendered one its own URL may answer no GET, so the host
// resolves the target (app.ts → switchBase) and this just renders it.
const locals = i18nLocals(request({ switchBase: "/admin/users/u1" }));
assert.deepEqual(locals.localeSwitch.map((c) => c.href), ["/admin/users/u1?locale=en-US", "/admin/users/u1?locale=sv-SE"]);
});
+4 -7
View File
@@ -32,7 +32,9 @@ export interface I18nRequest {
locale: string;
localeHref: (href: string) => string;
locales: string[];
method: string; // a page rendered in response to a POST has no linkable URL — see localeSwitch
// Where the language picker points — "this page", except after a POST, whose URL may answer no
// GET at all; the host then resolves the nearest page that does (app.ts → switchBase).
switchBase: string;
t: Translate;
url: URL;
}
@@ -50,11 +52,6 @@ export const ENGLISH_LOCALS: I18nLocals = {
};
export function i18nLocals(ctx: I18nRequest): I18nLocals {
const here = `${ctx.url.pathname}${ctx.url.search}`;
// The picker links to this same page in another language. After a POST that page's URL often has
// no GET at all (the admin's recovery-code screen, say), so linking there would dead-end on a 405
// — and on a re-rendered form it would silently discard what the user typed. Offer nothing.
const linkable = ctx.method === "GET" || ctx.method === "HEAD";
// ctx.localeHref is a no-op unless the URL asked for a locale, so it is also the honest answer to
// "did it?" — asking the function that decides keeps the two from drifting apart.
const carried = ctx.localeHref("/") === "/" ? null : ctx.locale;
@@ -63,7 +60,7 @@ export function i18nLocals(ctx: I18nRequest): I18nLocals {
locale: ctx.locale,
localeHref: (href) => ctx.localeHref(href),
localeParam: carried,
localeSwitch: linkable ? ctx.locales.map((tag) => ({ current: tag === ctx.locale, href: localeHref(here, tag), label: localeLabel(tag), tag })) : [],
localeSwitch: ctx.locales.map((tag) => ({ current: tag === ctx.locale, href: localeHref(ctx.switchBase, tag), label: localeLabel(tag), tag })),
locales: ctx.locales,
t: ctx.t,
};