Translations
How SpipCP is translated, what the tiers mean, and how to improve a language or add one.
SpipCP speaks seven languages. Three of them have been read by a person; the other three are machine drafts that get better every time somebody looks at one.
The list was longer. 1.6.0 shipped thirteen languages because the pipeline could produce thirteen — which is a reason to build a pipeline, not a reason to publish a catalog. A language on the list is a promise that somebody will read what it says, and 1.6.1 cut the list to the languages that promise can be kept for. The pipeline has not gone anywhere: adding a language back is one entry in the registry and a catalog the engine drafts in an afternoon.
This page is for the person who wants their language better — or present at all.
The three things that get translated
The interface. Every label, button, heading, empty state and error in the panel. The English
strings in the source code are the catalog: there are no invented key names, so editing a sentence
in the code invalidates its translations automatically. The catalogs are .po files under
panel/src/locales/<language>/messages.po, compiled into the build.
The documentation. These pages. A translated page sits beside its English source —
installation.fr.mdx next to installation.mdx — and the URL carries the language:
/docs/fr/installation. English stays unprefixed, so no existing link ever breaks. A page nobody has
translated yet shows the English, with a notice saying so; it is never a 404.
Changelog releases. A release is written by a human in the panel, so no catalog can hold it. It is translated once, when it is published, and stored — see Releasing.
The tiers, and what they mean
| Tier | Languages | What it means |
|---|---|---|
| Reviewed | French, German, Spanish | A person has read the translation. |
| Machine-first | Italian, Ukrainian, Chinese | A machine wrote it. Usable, and unreviewed. |
Nothing hides which is which. In the panel's language menu a machine-first language is marked
beta; in the documentation a machine-drafted page carries a notice at the top. A reader deciding
whether to trust a sentence about DNS delegation is entitled to know who wrote it before they read
it, not after.
An untranslated string always renders its English source. Never a blank, never a raw key.
Improving a translation
Interface strings live in the catalogs:
#. machine-translated (libretranslate) — review and clear the fuzzy flag
#: src/components/shell/nav.ts
#, fuzzy
msgid "Nodes"
msgstr "Serveurs"Edit the msgstr, remove the #, fuzzy line, and open a pull request. That flag is the review
queue: while it is there, the string is a machine draft and the fill script may rewrite it. Once it is
gone, the entry belongs to a human and nothing touches it again.
Documentation pages work the same way: edit the .mdx, remove machineTranslated: true from its
frontmatter, and open a pull request.
To see where a language stands:
pnpm lang:status # the panel's catalogs
pnpm docs:translate --check # the documentation (run in the docs repo)language reviewed machine english total
------------------------------------------
fr 2700 0 0 2700
de 2700 0 0 2700
es 0 2700 0 2700reviewed means a person approved it. machine is a draft awaiting one. english renders the
English source.
Guidance for reviewers
Use the formal register where a pronoun is unavoidable — vous, Sie, usted. The panel's own voice is impersonal and declarative, so most sentences have no pronoun at all; where one is forced, the formal form is the one that fits.
Do not translate these. They are the same word in every language, and translating them makes a sentence harder to act on, not easier:
SpipCP · DNS · TLS · SSL · HTTP-01 · DNS-01 · ACME · glue · apex · zone · semver · systemd · Docker · Compose · Caddy · PowerDNS · nginx · restic · WordPress · Git · SSH · IPv4 · IPv6
Keep sentences whole. If a message contains a placeholder — {name}, {count} — it must survive
the translation exactly as written. {nom} is not a placeholder; it will render as those five literal
characters to every reader, forever. The fill script refuses any translation that mangles one, and a reviewer should too.
Match the noun to the number. A message with a count carries plural forms. Ukrainian has more of them than English does; use the forms the language actually needs, not the two English happens to have.
Adding a language
The whole procedure, and there is no more to it:
- Add an entry to
panel/src/shared/locales.ts— the language key, its English name, its name in itself, a flag, andreviewed: false. - Add the same entry to
SpipCP-Docs/src/lib/languages.ts. - Run
pnpm lang:extractin the panel to create the catalog. - Optionally, draft it:
pnpm lang:translate --lang <code>andpnpm docs:translate --lang <code>.
No schema change, no migration, no new surface. The language menu picks it up because the menu is generated from the registry, and the documentation offers it because the build discovers which languages have pages by looking at the disk.
Right-to-left languages — Arabic, Hebrew — are not yet supported. Not because of the words: RTL is a layout audit of every row, chevron and chart in the panel, and shipping the text into a left-to-right shell would be worse than not shipping it. It is a known piece of work, not an oversight.
Translating is not a one-off
It is tempting to think of translation as a migration: run the script once, review the output, delete the tooling. It is not, and the tooling stays.
Every feature adds strings. A new page, a new dialog, a new error message — each one arrives in
English and each one is missing from six catalogs the moment it is written. pnpm check fails if a
new string has not been extracted (check-lang-clean), so the catalogs cannot silently fall behind;
but extracted is not translated. The loop is: write the feature in English, extract, draft the new
entries with the engine, review them.
Every release needs translating. A changelog release is written by a person, in the panel, after the image was built — so no catalog could ever have held it. Publishing translates it, so this looks after itself once an engine is configured; a release published before that, or while the engine was unset, stays English until the Translate action on the Changelog page clears the backlog. That action appears only while something is untranslated, and disappears once nothing is.
A language is never "finished". Clearing the last fuzzy flag in French means French is reviewed as of today. The next feature reopens it.
What does change over time is the tier: as a language's drafts get reviewed, its reviewed flag
flips in the registry and the beta mark disappears from the menu. That is the only thing that is ever
"done".
The translation engine
Machine drafts come from LibreTranslate, an open-source, self-hostable translation service. It is used at build time only, by two scripts:
LIBRETRANSLATE_URL=https://translate.example.com \
LIBRETRANSLATE_API_KEY=… \
pnpm lang:translate # fills empty catalog entries (panel repo)
pnpm docs:translate # drafts documentation pages (docs repo)Both write only into what is empty, flag everything they write as machine output, and never touch a translation a human owns. Both hold code, component tags, URLs and placeholders entirely out of the request — a guard that relies on the engine behaving is not a guard.
Neither script normally needs running by hand. pnpm deploy runs both as its first step, so a
deploy drafts whatever changed since the last one — the catalogs and the documentation together — and
commits each in its own repo. It is entirely gated on the engine being configured: with
LIBRETRANSLATE_URL unset the step is skipped and the deploy carries on with the drafts already
committed. A fill that fails never fails the deploy. Run the scripts directly only to draft without
deploying, or to re-run one surface on its own (pnpm docs:translate --lang it).
A translated page is only re-drafted when its source changes
Each translated page records a hash of the English it was built from, so a deploy costs only the pages that actually moved. The corollary is worth knowing: if a run fails partway through a page — an engine timeout mid-batch — that page is still written and stamped current, so a later run skips it and the untranslated segments stay English. Delete the affected page and re-run to force it.
The running panel never calls a translation service to render a page. The catalogs and the documentation are files, compiled into the image. The one exception is translating a changelog release at publish, which is opt-in, operator-triggered, stored once, and configured in Settings → Changelog. A panel with no engine configured makes no outbound translation request at all.