Architecture review fixes: partials carry the locale, mountable locales/, shared core words
CI / full-gate (push) Successful in 2m36s

This commit is contained in:
2026-08-03 23:12:18 +02:00
parent 245d1ad5b5
commit 6440c543e5
48 changed files with 337 additions and 118 deletions
+4 -4
View File
@@ -11,7 +11,7 @@ import type { Translate } from "../i18n/translate.ts";
import { type MenuConfig } from "./menu-config.ts";
import { composeNav, type NavNode } from "./nav.ts";
import type { Plugin } from "../plugin-host/plugin.ts";
import { shellUser, type ShellUser } from "./shell-context.ts";
import { branding, shellUser, type ShellUser } from "./shell-context.ts";
// The "Dashboard" link to the gated app home (/dashboard). It targets a gated route, so it's shown
// only to a signed-in user (an anonymous click would only dead-end at /login). Its label is a
@@ -62,15 +62,15 @@ export function buildPluginChrome(opts: ChromeOptions): PageChrome {
if (target) markCurrent(nav, target);
}
const b = opts.menu.branding;
// The sign-in link keeps the visitor's locale, and brings it back afterwards via return_to.
const returnTo = opts.currentPath ? `/login?return_to=${encodeURIComponent(carryLocale(opts.currentPath))}` : "/login";
const { theme, ...brand } = branding(opts.menu, t);
return {
brand: { ...(b.logo != null ? { logo: b.logo } : {}), name: t(b.name), ...(b.sub != null ? { sub: t(b.sub) } : {}) },
brand,
csrfToken: opts.csrfToken ?? "",
nav: carryLocaleInto(nav, carryLocale),
signInHref: carryLocale(returnTo),
...(b.theme != null ? { theme: b.theme } : {}),
...(theme != null ? { theme } : {}),
user: shellUser(opts.user, t),
};
}
+2 -1
View File
@@ -3,9 +3,10 @@ import { dirname, join } from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
import ejs from "ejs";
import { ENGLISH_LOCALS } from "../i18n/view-locals.ts";
const menu = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "views", "partials", "menu.ejs");
const render = (data: Record<string, unknown> = {}): Promise<string> => ejs.renderFile(menu, data);
const render = (data: Record<string, unknown> = {}): Promise<string> => ejs.renderFile(menu, { ...ENGLISH_LOCALS, ...data });
const flat = (s: string): string => s.replace(/>\s+</g, "><").replace(/\s+/g, " ").trim();
test("menu renders trigger, positioning, the item matrix and check groups", async () => {
+2 -1
View File
@@ -6,6 +6,7 @@
// the override (+ branding); this helper only transforms data, so its result is per-deployment
// up to the final permission filter and emits clean nodes ready for nav-tree.ejs (no id/permission).
import { ENGLISH } from "../i18n/english.ts";
import type { Translate } from "../i18n/translate.ts";
export interface NavNode {
@@ -42,7 +43,7 @@ export function composeNav(
fragments: NavNode[][] = [],
override: NavOverride = {},
permissions: string[] = [],
t: Translate = (key) => key,
t: Translate = ENGLISH,
): NavNode[] {
let nodes: NavNode[] = fragments.flat();
if (override.rename) nodes = renameTree(nodes, override.rename);
+15 -3
View File
@@ -10,6 +10,18 @@ import { ENGLISH } from "../i18n/english.ts";
import type { Translate } from "../i18n/translate.ts";
import { type MenuConfig } from "./menu-config.ts";
// The brand block both the chrome and the shell model carry. `name`/`sub` pass through `t`, so a
// catalog key is translated and an operator's own wording renders as written.
export function branding(menu: MenuConfig, t: Translate): { logo?: string; name: string; sub?: string; theme?: string } {
const b = menu.branding;
return {
...(b.logo != null ? { logo: b.logo } : {}),
name: t(b.name),
...(b.sub != null ? { sub: t(b.sub) } : {}),
...(b.theme != null ? { theme: b.theme } : {}),
};
}
export interface ShellUser {
email: string;
initials: string;
@@ -44,14 +56,14 @@ export function buildShellContext(opts: {
title: string;
user?: User | null;
}): ShellModel {
const b = opts.menu.branding;
const t = opts.t ?? ENGLISH;
const { theme, ...brand } = branding(opts.menu, t);
return {
brand: { ...(b.logo != null ? { logo: b.logo } : {}), name: t(b.name), ...(b.sub != null ? { sub: t(b.sub) } : {}) },
brand,
...(opts.breadcrumbs ? { breadcrumbs: opts.breadcrumbs } : {}),
csrfToken: opts.csrfToken ?? "",
...(opts.signInHref != null ? { signInHref: opts.signInHref } : {}),
...(b.theme != null ? { theme: b.theme } : {}),
...(theme != null ? { theme } : {}),
title: opts.title,
user: shellUser(opts.user, t),
};