complete i18n migration to /[locale]/ with EN+ES content

Full rewrite of the docs site under app/[locale]/ with next-intl
in localePrefix:"always" mode. Every page now exists at both
/en/<path> and /es/<path>; the root / shows a meta-refresh + JS
redirect to /<defaultLocale>/ so GitHub Pages serves something
on the apex URL.

Highlights:
- 107 doc pages migrated to file-per-page JSON namespaces under
  messages/en/ and messages/es/. Spanish content is fully
  translated (no copy-of-English placeholders).
- New documentation for the Active Suppressions section in the
  Settings tab and the per-event Dismiss dropdown in the Health
  Monitor modal.
- New screenshots: dismiss-duration-dropdown.png and an updated
  health-suppression-settings.png.
- Pagefind integrated for client-side search; index is built on
  every CI deploy (not committed).
- RSS feeds: per-locale at /<locale>/rss.xml plus root /rss.xml
  for backward compat.
- Removed the dead app/[locale]/guides/[slug]/ route — every
  guide now has its own static page and no markdown source
  remains.
- Fixed orphan link /guides/nvidia -> /guides/nvidia-manual in
  docs/hardware/nvidia-host.
- Removed obsolete components (footer2, calendar, drawer).

Verified locally with `npm ci && npm run build`: 2804 files in
out/, 231 pages indexed by pagefind, root redirect intact, both
locale roots and the new Active Suppressions docs render OK.
This commit is contained in:
MacRimi
2026-05-31 12:41:10 +02:00
parent 875910b4d7
commit 5ca3463bf6
649 changed files with 83958 additions and 11096 deletions
+329
View File
@@ -0,0 +1,329 @@
# Contributing translations
The ProxMenux documentation site is built with Next.js (App Router) and
serves every page under two URLs:
- `/en/<path>` — English, the source of truth
- `/es/<path>` — Spanish, in progress
We use [`next-intl`](https://next-intl.dev) for the i18n plumbing. Anyone
can translate the docs without writing TypeScript: most of the work is
filling in a JSON file. This guide explains the workflow end to end.
> **Default policy: small, focused PRs.** One page per pull request. Big
> bundles of "I translated 30 pages at once" are hard to review and
> merge cleanly when several contributors are working in parallel.
---
## What's already wired
Out of the box you get:
- Routing under `app/[locale]/...` — every page already renders at both
`/en/...` and `/es/...`.
- Locale-aware navigation via `@/i18n/navigation` (`<Link>`, `useRouter`,
`usePathname`). Use these instead of `next/link` for internal hrefs so
the active `[locale]` prefix is preserved.
- A language switcher in the navbar (`<LanguageSwitcher />`).
- Automatic message discovery: any JSON file under
`messages/<locale>/...` is loaded and merged into a single namespace
tree. **You never need to register a new file anywhere**, the build
picks it up automatically.
- Fallback to English when a translation is missing — pages render in
English instead of breaking with a `MISSING_MESSAGE` placeholder.
---
## File layout
```
web/
├── i18n/
│ ├── routing.ts # supported locales + default
│ ├── request.ts # per-request config (uses loadMessages)
│ ├── loadMessages.ts # walks messages/<locale>/ and builds the tree
│ └── navigation.ts # locale-aware Link, useRouter, etc.
├── messages/
│ ├── en/
│ │ ├── common.json # shared strings (nav, footer, language switcher)
│ │ └── docs/
│ │ └── monitor/
│ │ └── index.json # page-specific strings for /docs/monitor
│ └── es/
│ ├── common.json
│ └── docs/
│ └── monitor/
│ └── index.json
└── app/[locale]/
└── docs/
└── monitor/
└── page.tsx # the page itself
```
### Naming convention
- `common.json` and `index.json` at any folder level → keys are merged
at the current namespace level (no extra nesting). Use these for
"this whole folder" or "this whole section" defaults.
- `<name>.json` at any folder level → keys go under the `<name>`
namespace.
- Sub-directories nest as additional namespaces, with **kebab-case
converted to camelCase** in the JS API. So
`messages/en/docs/monitor/access-auth.json` is consumed as
`getTranslations({ namespace: 'docs.monitor.accessAuth' })`.
---
## Workflow: translate one page
### 1. Pick a page
Browse `app/[locale]/docs/` and find a page that:
- Has no entry yet under `messages/es/<same-path>/` (Spanish), **and**
- Is not already mid-translation by someone else (check open PRs).
If you're translating to a new locale, start with the smallest pages so
you can submit early PRs and get feedback before tackling the big ones.
### 2. Check whether the page is already i18n-ready
There are two cases:
**Case A — the page already uses `getTranslations()`** (look for an
`import` from `next-intl/server` and `t()` calls in JSX). Your job is
straightforward: only create the `messages/<locale>/<path>.json` file
with the translated strings. **You don't touch the `.tsx` file at all**.
**Case B — the page still has hard-coded English strings in JSX.** You
need both:
1. Refactor the page to read its strings from a JSON file (this part
touches the `.tsx`).
2. Provide the English JSON (the source of truth) **and** the
translated JSON.
The pilot page `app/[locale]/docs/monitor/page.tsx` is the reference
implementation for case B — copy its patterns.
### 3. Add the JSON
Create `messages/<locale>/<same-path-as-page>.json` (or `index.json` if
the page is the section index). Mirror the English file's structure
exactly — every key must exist in both, only the values change.
If a key contains inline HTML-style tags (`<code>`, `<strong>`,
`<em>`, `<link>`, `<linkApi>`, etc.), keep them in the same positions
in your translation. They're not real HTML — they're placeholders that
the page renders via `t.rich()` and substitutes for real React nodes.
Example:
```json
{
"intro": "Eight first-class sections, backed by their own API endpoints."
}
```
```json
{
"intro": "Ocho secciones principales, respaldadas por sus propios endpoints de API."
}
```
```json
{
"footer": "See the <link>Architecture</link> page for details."
}
```
```json
{
"footer": "Mira la página de <link>Architecture</link> para más detalles."
}
```
### 4. Test locally
```bash
cd web
npm run dev
```
Open `http://localhost:3000/<locale>/<page-path>` and check that:
- All your translated strings render correctly.
- No `MISSING_MESSAGE` text appears (means a key is in the page's
`.tsx` but missing from your JSON).
- The page still passes `npm run build` cleanly.
### 5. Open the PR
One page per PR is the convention. Title format:
```
docs(i18n/<locale>): translate <route>
```
Examples:
- `docs(i18n/es): translate /docs/monitor`
- `docs(i18n/fr): translate /docs/monitor/notifications`
In the description, mention which page you translated and whether you
also had to refactor the `.tsx` (case B) or only added JSON (case A).
---
## Workflow: convert a page from hard-coded English to i18n (case B)
This is the more involved path. Use the pilot
`app/[locale]/docs/monitor/page.tsx` as the reference.
### High-level changes to the `.tsx`
1. Make the page an **async Server Component** that receives
`params: Promise<{ locale: string }>`.
2. Add `generateStaticParams()` that returns one entry per locale (see
`routing.locales` in `i18n/routing.ts`).
3. If the page exports `metadata`, replace it with `generateMetadata()`
that reads from `getTranslations({ namespace: '<page>.meta' })`.
4. Call `setRequestLocale(locale)` near the top of the component body.
5. Call `await getTranslations({ locale, namespace: 'docs.<section>.<page>' })`
and rename it to `t`.
6. Replace every English string in JSX with `t('key')` (plain text) or
`t.rich('key', { code, strong, em, link, ... })` for strings with
inline tags.
7. For lists / tables of structured items (e.g. table rows, nav items),
pull the array from `getMessages()` and iterate.
### JSON file structure
Use the pilot's `messages/en/docs/monitor/index.json` as the template.
The high-level shape is:
```json
{
"meta": {
"title": "...",
"description": "...",
"ogTitle": "...",
"ogDescription": "...",
"twitterTitle": "...",
"twitterDescription": "..."
},
"header": {
"title": "...",
"description": "...",
"section": "..."
},
"<section1>": { ... },
"<section2>": { ... }
}
```
Group keys by the section they appear in. Keep nesting shallow (2-3
levels max) so the JSON stays readable for translators.
### Rich-text placeholder tags
When a paragraph contains inline elements like `<code>` or `<strong>`,
encode them in the JSON exactly as you want them to appear — but
remember they're **placeholders**, not real HTML. The `.tsx` registers
React renderers for each tag name in the call:
```tsx
t.rich("intro", {
code: (chunks) => <code>{chunks}</code>,
strong: (chunks) => <strong>{chunks}</strong>,
link: (chunks) => (
<Link href="/docs/monitor" className="text-blue-600 hover:underline">
{chunks}
</Link>
),
})
```
Translators just have to keep the tags in roughly the same positions.
Don't introduce new tag names in the JSON unless the `.tsx` also
registers a renderer for them.
---
## What's intentionally NOT translatable
- **Code blocks** (inside `CopyableCode` etc.) — only the comment lines
(`# Comment here`) should be moved to JSON if they're explanatory.
Don't translate code keywords, command names or paths.
- **URLs and paths** (`/docs/monitor`, `https://github.com/...`) —
these stay identical across locales.
- **External link labels** like "GitHub", "ProxMenux" — proper nouns
and product names stay in their original form.
- **Variable names, environment variables, file names** (`auth.json`,
`MONITOR_VERSION`, `/var/log/journal/`) — never translated.
---
## Adding a new locale
If you want to add a language that isn't in the project yet:
1. Add the locale code to `routing.ts`:
```ts
export const routing = defineRouting({
locales: ["en", "es", "fr"], // add your code here
defaultLocale: "en",
localePrefix: "always",
})
```
2. Create the `messages/<locale>/` folder.
3. Copy `messages/en/common.json` over and translate it. **This is
mandatory** — without it the navbar and footer fall back to English
on every page.
4. Start translating individual pages one PR at a time.
5. Mention in your first PR that you're seeding the locale so reviewers
know to expect a follow-up batch.
---
## FAQ
### My translated page still shows English text. Why?
Three common causes:
1. The page file is **case A** (uses `getTranslations()`) and your JSON
path doesn't match the namespace it expects. Check the page's
`getTranslations({ namespace: '...' })` call and mirror it in your
folder structure.
2. The page is **case B** (still hard-coded). It needs the `.tsx`
refactored first — that part is a developer task, not a translator
task.
3. The dev server is serving cached output. Stop it (Ctrl+C), remove
`web/.next/`, and run `npm run dev` again.
### What about translations of the Monitor (the AppImage), not just the docs?
This guide only covers the **public documentation site** in `web/`.
The Monitor's dashboard UI in `AppImage/` is a separate project and
not currently i18n-enabled. Translating the Monitor would require a
parallel effort.
### Where can I see what's missing?
Compare the directory trees:
```bash
diff -rq web/messages/en web/messages/es
```
Anything listed as "Only in en" still needs a Spanish version. (Swap
`es` for your locale.)
### My PR conflicts with another translator's PR.
Because we keep one page per PR, the only realistic conflict zone is
`messages/<locale>/common.json` (shared strings) or
`app/[locale]/docs/<section>/page.tsx` (refactored at the same time).
Rebase on `develop` and re-resolve; ping the other contributor in the
PR thread if the merge is non-obvious.