Full Spanish locale for CyberPanel 3.0.3 — 3,303 messages, zero untranslated, zero fuzzy — plus the tooling to regenerate it after an upgrade.
$ msgfmt --statistics -o /dev/null locale/es/LC_MESSAGES/django.po
3303 translated messages.
No untranslated line, no fuzzy line. That's the whole claim.
Sidebar, breadcrumbs, empty states and quick actions — all from the catalogue.
![]() |
![]() |
| Every field label, placeholder and helper line on the create-website form. | Mail account creation, including the per-domain selector. |
Captured from a CyberPanel 3.0.3 install running this catalogue, signed in as a restricted user with no websites — so no domains appear. The server's IP was replaced with the documentation range
203.0.113.0/24before publishing. The panel in these shots carries a custom theme; the translation itself is theme-independent.
Selecting Spanish in CyberPanel leaves most of the interface in English. It looks like missing translations. It isn't — or rather, it's three separate problems that happen to share a symptom:
| Cause | Entries | Obvious? |
|---|---|---|
| Empty strings | 634 | Yes |
Marked #, fuzzy |
378 | No — they were written, then discarded |
| Absent from the catalogue | ~2,000 | No — the .po was years behind the templates |
This is the one that wastes your afternoon. msgfmt skips every entry marked
#, fuzzy by default, and says nothing about it. A .po that looks translated
when you read it compiles to a .mo that is mostly empty.
msgfmt --statistics -o /dev/null django.po
# 1291 translated messages, 1200 fuzzy translations, 812 untranslated messages.
# ^^^^^^^^^^^^^^^^^^^^^^^ these do not shipCyberPanel 3.0.3's templates contain 3,303 translatable strings. The shipped
.po covered roughly a third. Whole screens — Docker, incremental backups, webmail,
container management — were added over the years without ever regenerating the
locales. Those strings had to be extracted and written from scratch.
The tempting one-liner is msgattrib --clear-fuzzy. Don't.
Fuzzy means gettext guessed by string similarity — it isn't a partial translation, it's a translation of a different sentence. Clearing the flags activates confidently wrong text:
| English | What the fuzzy entry said | Actual meaning |
|---|---|---|
List Users |
Modificar Usuario | Modify User |
Manage Services |
Opciones SSL | SSL Options |
Cancel |
Cancelar Copia de seguridad | Cancel Backup |
Save Rewrite Rules |
Cambios Guardados | Changes Saved |
141 of these were corrected by hand. The remaining ~1,200 were retranslated from scratch rather than trusted — slower, and the only defensible choice.
Without it, Django scans site-packages and pulls ~290 of its own internal strings
into your catalogue. That isn't just noise: if any of them end up fuzzy, they
override the correct translations Django already ships.
python manage.py makemessages -l es --no-wrap \
-i node_modules -i venv -i lib -i bin -i lib64 --no-obsoleteYou can spot the problem by symptom: if Enter a valid URL or
%(model_name)s with this %(field_labels)s already exists show up as untranslated,
you're scanning Django itself.
# 1 · Back up the existing catalogue
cp /usr/local/CyberCP/locale/es/LC_MESSAGES/django.po ~/django.po.bak
# 2 · Drop this one in
cp locale/es/LC_MESSAGES/django.po /usr/local/CyberCP/locale/es/LC_MESSAGES/
# 3 · Compile and restart
cd /usr/local/CyberCP/locale/es/LC_MESSAGES
msgfmt --statistics -o django.mo django.po
systemctl restart lscpdThen hard-refresh the browser. Verify with:
msgfmt --statistics -o /dev/null django.po # expect: 3303 translated messages.A CyberPanel upgrade overwrites
locale/. Keep this repo around and repeat the three steps, or use the tooling below if the upgrade added new screens.
# Extract what is missing
cd /usr/local/CyberCP/locale/es/LC_MESSAGES
msgattrib --untranslated --no-obsolete --no-wrap django.po > untranslated.po
msgattrib --only-fuzzy --no-obsolete --no-wrap django.po > fuzzy.po
# Pull the msgids out into a plain list
python3 tools/extract.py # -> pending.json
# Translate, then write them back (clears fuzzy flags as it goes)
python3 tools/apply.py # dict.json + django.po -> django.po
# Deduplicate and compile
msguniq --use-first --no-wrap -o django.po django.po
msgfmt --statistics -o django.mo django.poapply.py escapes quotes and backslashes correctly and verifies that every
placeholder count matches between source and translation — a mismatched %s
raises at runtime, not at compile time, so it has to be caught here.
- A duplicate
msgidbreaks compilation. CyberPanel's original ships"Remote Backups"twice. Fix withmsguniq --use-first. - Placeholders must match exactly.
%s,%(name)sand%%need identical counts on both sides or Django raises while rendering the page. - Some
{% trans %}tags use single quotes, some have trailing whitespace.{% trans "ModSecurity is not installed " %}— note the space. Any homemade grep-based coverage count will be wrong.msgfmt --statisticsis the only measurement that counts what actually ships.
locale/es/LC_MESSAGES/django.po the catalogue
tools/extract.py pull pending msgids out of msgattrib output
tools/apply.py write translations back, clear fuzzy, check placeholders
docs/method.md the full procedure
The original Spanish translation is by Francisco Galán, 2018. This repository is a 2026 revision that completes it for CyberPanel 3.0.3.
CyberPanel is licensed GPL-3.0. A translation catalogue derived from its own
.po is a derivative work, so this repository carries the same license — see
LICENSE.



