mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-01 04:54:42 +00:00
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:
@@ -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.
|
||||
Reference in New Issue
Block a user