Pin the chrome's locale carrying in unit tests; keep one carrier list
CI / full-gate (push) Successful in 2m36s

This commit is contained in:
2026-08-03 23:25:54 +02:00
parent b3df7084c4
commit 2b20497785
4 changed files with 24 additions and 7 deletions
+2 -1
View File
@@ -111,7 +111,8 @@ them. Revisit only if the stated reason stops holding.
locales than the host (its strings fall back to `en-US` per key), never one the host lacks.
- **The core building blocks carry the locale; a plugin doesn't have to.** The shell (breadcrumbs),
`pagination`, `filter-bar`, `data-table`, `auth-card`, `flow-body`, `field` and `menu` wrap every
href they render in `localeHref`; the nav is wrapped upstream in `chrome.ts`; and the two GET forms
href they render in `localeHref`; the nav and the sign-in link are wrapped upstream in `chrome.ts`;
and the two GET forms
(filter bar, rows-per-page) carry it as a hidden `locale` input, since a GET submit replaces the
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.
+5 -6
View File
@@ -998,12 +998,11 @@ Three rules worth knowing:
that deliberately carries markup is rendered with `<%- %>` — and must never interpolate
untrusted data, since nothing escapes it there.
- **Dates and numbers are `Intl`'s job**, not the catalog's: `new Intl.DateTimeFormat(ctx.locale)`.
- **The core building blocks carry the locale for you.** The shell's breadcrumbs, `pagination`,
`filter-bar`, `data-table`, `auth-card`, `field` and the nav wrap every href they render, and the
two GET forms (filter bar, rows-per-page) carry it as a hidden `locale` input — a GET submit
replaces the whole query string, so no href wrapper can reach it. `ctx.localeHref` is only 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.
- **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.
- **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.
+8
View File
@@ -52,3 +52,11 @@ test("field defaults to a bare text input, escapes a string error, and never thr
assert.match(stringErr, /<span>&lt;b&gt;Required&lt;\/b&gt;\.<\/span>/); // string error is escaped
assert.match(stringErr, /aria-describedby="x-err"/);
});
test("an inline field link carries the visitor's language", async () => {
const html = await ejs.renderFile(field, {
...ENGLISH_LOCALS, localeHref: (href: string) => `${href}?locale=sv-SE`,
id: "password", label: "Password", link: { href: "/recovery", label: "Forgot password?" }, name: "password",
});
assert.match(html, /href="\/recovery\?locale=sv-SE"/);
});
+9
View File
@@ -5,6 +5,9 @@ import { fileURLToPath } from "node:url";
import ejs from "ejs";
import { ENGLISH_LOCALS } from "../i18n/view-locals.ts";
// A localeHref that marks what it touches, so a raw href in the chrome is visible to a test.
const CARRYING = { ...ENGLISH_LOCALS, localeHref: (href: string) => `${href}?locale=sv-SE` };
const shell = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "views", "partials", "shell.ejs");
const render = (data: Record<string, unknown> = {}): Promise<string> => ejs.renderFile(shell, { ...ENGLISH_LOCALS, ...data });
@@ -107,3 +110,9 @@ test("app shell escapes text but passes slot HTML through, and renders with defa
assert.match(bare, /<aside class="sidebar"/);
assert.match(bare, /<main class="content"/);
});
test("the chrome carries the visitor's language: breadcrumb links go through localeHref", async () => {
const html = await ejs.renderFile(shell, { ...CARRYING, breadcrumbs: [{ href: "/admin/users", label: "Users" }, { label: "Ada" }], title: "Ada" });
assert.match(html, /<a href="\/admin\/users\?locale=sv-SE">Users<\/a>/);
assert.match(html, /<span>Ada<\/span>/); // the current crumb has no href to carry
});