SpipCP
Contributing

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

TierLanguagesWhat it means
ReviewedFrench, German, SpanishA person has read the translation.
Machine-firstItalian, Ukrainian, ChineseA 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        2700

reviewed 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:

  1. Add an entry to panel/src/shared/locales.ts — the language key, its English name, its name in itself, a flag, and reviewed: false.
  2. Add the same entry to SpipCP-Docs/src/lib/languages.ts.
  3. Run pnpm lang:extract in the panel to create the catalog.
  4. Optionally, draft it: pnpm lang:translate --lang <code> and pnpm 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.

On this page