# Contributing translations The ProxMenux documentation site is built with Next.js (App Router) and serves every published page under locale-prefixed URLs: - `/en/` — English, the source of truth - `/es/` — Spanish - `/de/` — German - `/fr/` — French - `/it/` — Italian - `/pt/` — Portuguese - `/sk/` — Slovak, with English fallback where needed - `/sv/` — Swedish 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 renders for every locale enabled in `i18n/routing.ts`. - Locale-aware navigation via `@/i18n/navigation` (``, `useRouter`, `usePathname`). Use these instead of `next/link` for internal hrefs so the active `[locale]` prefix is preserved. - A language switcher in the navbar (``). - Automatic message discovery: any JSON file under `messages//...` 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// 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 │ └── sk/ │ └── ... └── 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. - `.json` at any folder level → keys go under the `` 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///`, **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//.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//.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 (``, ``, ``, ``, ``, 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 Architecture page for details." } ``` ```json { "footer": "Mira la página de Architecture para más detalles." } ``` ### 4. Test locally From the repository root, check the available catalogs without using a translation provider: ```bash python3 .github/scripts/check_web_docs_catalogs.py ``` For a full locale-completion change, add `--require-complete` to also require every English file and leaf. This validates schema, placeholders, rich-text tags, literal code and rendering metadata; it does not replace linguistic review. Partial translations can continue to use the English fallback. ```bash cd web npm run dev ``` Open `http://localhost:3000//` 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/): translate ``` 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). --- ## Automated baseline and incremental updates The `Build web documentation translations` GitHub Action can create a machine-translated baseline and fill newly added English keys later. It uses `.github/scripts/build_web_docs_i18n.py` and supports a locale list, a file or directory scope, a per-locale file limit and a dry run. The builder is resumable and writes each completed JSON file atomically. By default it preserves every non-empty translated value, including wording reviewed by native speakers. It also protects rich-text tags, placeholders, URLs, paths, commands, variables and official product names before sending a string to the translation provider. `--refresh` deliberately overwrites the selected scope and should only be used when those translations are going to be reviewed again. Run a coverage report without contacting a translation service: ```bash python .github/scripts/build_web_docs_i18n.py \ --languages de,fr,it,pt,sk,sv \ --section docs \ --check ``` Translate a small resumable batch locally: ```bash python .github/scripts/build_web_docs_i18n.py \ --languages de \ --section docs/monitor \ --max-files 5 ``` Machine translation is a starting point, not the final authority. Native contributors can edit the generated JSON normally; later incremental runs will keep their non-empty wording unchanged. --- ## 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: '.meta' })`. 4. Call `setRequestLocale(locale)` near the top of the component body. 5. Call `await getTranslations({ locale, namespace: 'docs.
.' })` 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": "..." }, "": { ... }, "": { ... } } ``` 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 `` or ``, 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) => {chunks}, strong: (chunks) => {chunks}, link: (chunks) => ( {chunks} ), }) ``` 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. --- ## Language style: Spanish (es) The same criterion applies to the Monitor (`AppImage/messages/es`), the terminal menus (`lang/es.json`) and the documentation (`web/messages/es`). It is Spain Spanish, written with rigour but without formality. - **Address the reader as *tú*, never *usted*:** «Introduce la ruta», «Comprueba la conexión», «Déjalo vacío», «tus backups». - **Buttons, menu options, tab names and short action labels use the infinitive, without address:** «Guardar», «Eliminar la GPU de la configuración», «Activar 2FA», «Añadir GPU a un LXC». A full instruction sentence uses *tú*: «Selecciona una opción». - **Status messages say what happened:** «Backup creado», «No se necesitan cambios». - **Spain vocabulary:** *pulsar* (not *presionar*), *introducir* (not *ingresar*), *volver* (not *regresar*), *elegir* or *seleccionar*, *hacer clic*, *añadir* (not *agregar*), *fallo* (not *falla* as a noun), *usuario* (not *operador*), *monitorización* (not *monitoreo*). The key is **Intro**, not *Enter* or *Entrar*. - **Destructive actions use *eliminar*,** not *quitar*. - **Activate and deactivate:** an action or switch of the user is *activar/desactivar*; a technical state can stay *habilitado/deshabilitado*. - **Kept in English:** backup, snapshot, host, pool, slot, datastore, the application categories of OCI manager Apps and Helper-Scripts, product names, and the labels of other software as they appear on its screen (for example `Datacenter > Storage` in the Proxmox web interface). - **Translated:** *pila* (stack), *puente* (bridge), *pasarela* (gateway) when they are common nouns; *Secure Gateway* is a product name and stays as it is. - **One canonical form for repeated phrases,** for example «Pulsa Intro para volver al menú…». - **Documentation quotes screen texts exactly as the Spanish interface shows them,** and page titles end in «Documentación de ProxMenux». --- ## Adding a new locale If you want to add a language that isn't in the project yet: 1. Create `messages//common.json` and the page catalogs. The automated builder can provide the initial baseline when explicitly dispatched. It does not run on merge: changing English and its translations together must not trigger machine retranslation of the reviewed text. Offline validation still runs automatically for website changes. 2. Review the shared navigation and a representative set of documentation pages with a native speaker. 3. Add the locale code to `routing.ts` only when the locale is ready to be exposed publicly: ```ts export const routing = defineRouting({ locales: ["en", "es", "fr"], // add your code here defaultLocale: "en", localePrefix: "always", }) ``` 4. Add its human-readable label to the language switcher. 5. Continue reviewing individual pages one PR at a time. 6. 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 dashboard uses the separate catalogs under `AppImage/messages/` and its own translation workflow. ### 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//common.json` (shared strings) or `app/[locale]/docs/
/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.