From 1b2eff9cafb6042592cd4bb0b1daf9955548fbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 22 May 2026 22:58:09 +0200 Subject: [PATCH] feat(admin): multilingual admin UI with per-user language Make the server-rendered admin panel multilingual in the 13 locales shipped by perry/landing (en, de, es, fr, it, ja, ko, pt, th, tr, vi, id, zh-Hans). Each user picks their display language; it persists to users.locale. - src/admin/i18n: bundled per-locale TS message bags (en is source of truth + runtime fallback), t()/t.plural() translator, locale helpers (coerceLocale, negotiateLocale), and per-request middleware resolving user.locale -> skelpoAdminLang cookie -> Accept-Language -> en. - Externalize every visible admin string (nav, dashboard, content editor, settings, media incl. inline-JS prompts, forms, users, redirects, menus, jobs, webhooks, flash messages, status badges, ) to t(). Data/API paths/capability names stay verbatim. - /admin/profile page + sidebar quick-switcher to set the language. - CLI: users create/update gain --locale (lockstep with the API). - Tests: unit coverage for translator, negotiation, and key parity across all 12 locales (320 keys each). --- CLAUDE.md | 8 + src/admin/contentEditor.tsx | 126 +++++---- src/admin/i18n/index.ts | 101 ++++++++ src/admin/i18n/locales.ts | 87 +++++++ src/admin/i18n/messages/de.ts | 385 ++++++++++++++++++++++++++++ src/admin/i18n/messages/en.ts | 395 ++++++++++++++++++++++++++++ src/admin/i18n/messages/es.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/fr.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/id.ts | 385 ++++++++++++++++++++++++++++ src/admin/i18n/messages/index.ts | 36 +++ src/admin/i18n/messages/it.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/ja.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/ko.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/pt.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/th.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/tr.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/vi.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/messages/zh-Hans.ts | 384 +++++++++++++++++++++++++++ src/admin/i18n/middleware.ts | 36 +++ src/admin/layout.tsx | 77 ++++-- src/admin/routes.tsx | 327 +++++++++++++++-------- src/admin/screens.tsx | 399 +++++++++++++++-------------- src/app.ts | 2 + src/cli/commands.ts | 11 +- tests/unit/admin-i18n.test.ts | 88 +++++++ 25 files changed, 5921 insertions(+), 382 deletions(-) create mode 100644 src/admin/i18n/index.ts create mode 100644 src/admin/i18n/locales.ts create mode 100644 src/admin/i18n/messages/de.ts create mode 100644 src/admin/i18n/messages/en.ts create mode 100644 src/admin/i18n/messages/es.ts create mode 100644 src/admin/i18n/messages/fr.ts create mode 100644 src/admin/i18n/messages/id.ts create mode 100644 src/admin/i18n/messages/index.ts create mode 100644 src/admin/i18n/messages/it.ts create mode 100644 src/admin/i18n/messages/ja.ts create mode 100644 src/admin/i18n/messages/ko.ts create mode 100644 src/admin/i18n/messages/pt.ts create mode 100644 src/admin/i18n/messages/th.ts create mode 100644 src/admin/i18n/messages/tr.ts create mode 100644 src/admin/i18n/messages/vi.ts create mode 100644 src/admin/i18n/messages/zh-Hans.ts create mode 100644 src/admin/i18n/middleware.ts create mode 100644 tests/unit/admin-i18n.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index a51cd77..cb2fc17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,6 +120,14 @@ and is more lenient, so **a clean pass locally does not prove Node 22**. bodies stored as markdown text, rendered via `renderMarkdown`. - Maintenance/preview mode: admin toggle → frontend serves a branded "we'll be right back" 503; `?preview=` cookie bypasses it. +- **Admin is multilingual** (13 locales, mirrors perry/landing). UI strings + live in bundled per-locale TS bags under `src/admin/i18n/messages/` + (`en.ts` is source of truth, en is the runtime fallback); never hardcode + user-facing chrome — use `t('key')`/`t.plural(...)` from `getT(c)`. + Per-request locale = `users.locale` → `skelpoAdminLang` cookie → + Accept-Language (`src/admin/i18n/middleware.ts`, mounted on `adminRoutes`). + Users set their own language at `/admin/profile` + the sidebar switcher. + Adding a locale = extend `i18n/locales.ts` + add a `messages/.ts`. ## Permissions diff --git a/src/admin/contentEditor.tsx b/src/admin/contentEditor.tsx index 3c303e9..5a61edd 100644 --- a/src/admin/contentEditor.tsx +++ b/src/admin/contentEditor.tsx @@ -10,6 +10,8 @@ import type { FC } from 'hono/jsx'; import type { FieldDef, ContentTypeRow } from '../content/types.js'; import type { ContentDbRow } from '../content/content.js'; import { AdminPage, StatusBadge } from './layout.js'; +import { makeT, type Translator } from './i18n/index.js'; +import { defaultAdminLocale } from './i18n/locales.js'; function val(fields: Record, name: string): string { const v = fields[name]; @@ -29,12 +31,12 @@ function galleryStr(v: unknown): string { // JS-light: a hidden