diff --git a/AGENTS.md b/AGENTS.md index 4152809..2fe029e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,7 @@ A collaborative wiki of math concepts. Articles are written in Markdown with LaTeX (KaTeX) and stored in MongoDB behind an Express API; a Nuxt 4 app renders and edits them. +The interface itself speaks five languages (en, es, ca, fr, de), chosen per account. ``` mathew/ @@ -46,7 +47,9 @@ Plain JavaScript, ESM (`"type": "module"`, `.js` files with relative `.js` impor `User` (username + `passwordHash`, `select: false`; `setPassword`/`checkPassword` via bcrypt, `toPublic()` for responses, and `credentialProblem()` as the single place account rules are checked). The profile picture lives on `User.avatar` as a - small data URL — no file storage. `User.role` is `'admin'` or `'member'` + small data URL — no file storage. `User.locale` is the interface language the + person picked (`SUPPORTED_LOCALES`: `en es ca fr de`, default `'en'`). + `User.role` is `'admin'` or `'member'` (`ROLES`); `User.hasNone()` and `User.ensureAnAdmin()` cover who gets the former — see the roles gotcha below. - `src/routes/` — one Express `Router` per resource, mounted in `index.js`. @@ -55,7 +58,8 @@ Plain JavaScript, ESM (`"type": "module"`, `.js` files with relative `.js` impor `POST /api/auth/login`, `GET /api/auth/me` (who a token belongs to). Login answers the same way to a wrong name and a wrong password. Account updates, all behind `requireAuth`: `PUT /api/auth/me` (rename), `PUT /api/auth/password` - (needs the current one), `PUT /api/auth/avatar` (data URL, `""` clears). + (needs the current one), `PUT /api/auth/avatar` (data URL, `""` clears), + `PUT /api/auth/locale` (interface language, one of `SUPPORTED_LOCALES`). - `src/routes/admin.js` — mounted at `/api/admin`, everything behind `requireAuth, requireAdmin`: `GET /api/admin/users` (every account, newest first, with `createdAt`), `PUT /api/admin/users/:id/role` (`role` is `'admin'` @@ -91,6 +95,16 @@ the accent themes generate from a map in `_tokens.scss`. Colours stay CSS custom properties so themes swap at runtime via `data-theme`/`data-accent` attributes (set pre-paint by an inline script in `nuxt.config.ts`). +UI wording is localized with `@nuxtjs/i18n` (`frontend/i18n/`): `i18n.config.ts` plus +one JSON catalog per language in `i18n/locales/` (all 309 keys, kept in parity with +`en.json`). `strategy: 'no_prefix'` — the interface language never appears in a URL +(`/wiki//` is the *article's* language) — with cookie detection +(`mathew-locale`, `fallbackLocale: 'en'`) so the server renders in the right language +from the first draw. Every UI string goes through `t()` in the catalogs; messages that +carry ``/`` markup are drawn with `v-html` through `th()` +(`useAppLocale`), which escapes interpolated values. Adding a language = add a catalog +copy + an `APP_LANGUAGES` entry + the code in the backend's `SUPPORTED_LOCALES`. + - `app/pages/` — `/` (search), `/wiki/[slug]` (article + TOC), `/wiki/[slug]/edit`, `/new`, `/admin` (the dashboard: every account and its role, plus the buttons that hand the role on or take it back — an admin alone sees anything but a refusal here). @@ -117,9 +131,13 @@ properties so themes swap at runtime via `data-theme`/`data-accent` attributes profile popup), `useArticleActionsMenu` (same, for an article's ⋮ menu), and `useAccountActionsMenu` (same, for a dashboard row's ⋮ — it holds the **id** of the row that is open, so one row's menu is open at a time rather than a - boolean per row). + boolean per row), and `useAppLocale` (the interface language: `APP_LANGUAGES` — + the five supported UI languages with their own names —, `choose(code)` to turn + the UI (`setLocale` also keeps the `mathew-locale` cookie in step), `applyAccount(code)` + to let an account overrule the device, and `th()` for messages drawn as HTML). - `app/plugins/auth.ts` — runs `restoreSession()` once at startup on server and client, - so the sidebar's account row and the editor's gate are right in the first render. + so the sidebar's account row and the editor's gate are right in the first render, + and applies the account's `locale` when it restores one. - `app/utils/markdown.ts` — the single render pipeline: `markdown-it` (**html: false**, raw HTML is escaped) + `markdown-it-texmath` + KaTeX (`$...$` inline, `$$...$$` display, macros `\R \N \Z \Q \C`) + highlight.js, sanitized with DOMPurify. It also builds the @@ -128,6 +146,18 @@ properties so themes swap at runtime via `data-theme`/`data-accent` attributes ## Gotchas +- The **interface language** is not the article language: `/wiki/fr/…` addresses the + French *article*, never the UI. UI precedence: the signed-in account's `locale` (applied + at startup and on signing in) > the `mathew-locale` cookie (written by `setLocale`, read + on the server pre-render) > browser > `en`. The settings popup always turns this device; + with a session it also saves to the account — an account that refuses (dead session) + doesn't undo the switch. Error messages that come back from the API stay English; only + the frontend's own fallbacks are translated. +- Never call `t()` in `withDefaults` static prop defaults — they don't react to locale + changes. Use `props.x ?? t(...)` in the template/computed instead (see `AuthGate`, + `ConfirmModal`). +- Message catalogs are compiled: literal `{`/`@` break the compiler, so the editor's LaTeX + seed lives as a JS constant in `ArticleForm.vue` and only its words are catalog keys. - Reading is open; **writing takes an account**. `POST /api/articles` and `PUT`/`DELETE` by slug need `Authorization: Bearer ` from `/api/auth/login` or `/api/auth/register`. The frontend keeps that token in the `mathew-session` cookie