A catalog whose filename does not match ^[a-z]{2}(-[A-Z]{2})?$ is ignored
silently by available_languages() — no error, no log line, no entry in the
dropdown. pt-br.json, hu_HU.json and hungarian.json all fail that way, and the
guide gave no way to find out. Verified against the running container: hu.json
appeared as "Magyar" with three keys and the rest falling back to English,
while pt-br.json, hungarian.json and a de.json with a broken body were all
skipped without affecting the app.
Also spells out that app/translations/ lives inside the image and needs a
rebuild, so the config-directory drop-in is the one to reach for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.9 KiB
Translating mc-webui
The interface can be translated into any language. Translations live in a single JSON file per language, so adding one needs no rebuild, no restart and no compile step — drop the file on your server and pick it in Settings → Appearance → Language.
Built-in: English (en, the source) and Polish (pl). Everything else comes
from the community.
The backend stays English. This system translates the interface only. Error and status messages produced by the server (roughly one toast in seven, mostly on error paths) will still appear in English. That is deliberate, not a gap in your translation.
1. Read this first: what must NOT be translated
Mesh operators use the English protocol terms whatever language their UI is in. A
translated "flood" matches no firmware documentation, no meshcore-cli output and no
forum post — it just makes the app harder to use.
Leave these in English, always:
| Mode names shown as values | Flood, Direct, FLOOD — these are states the device reports, not descriptions |
| Protocol and radio terms | hop, advert, ACK, RSSI, SNR, LoRa, MQTT, broker, telemetry, pubkey, hash, spreading factor, bandwidth, coding rate |
| Node roles | Companion, Repeater, Room Server, Sensor — and the codes COM, REP, ROOM, SENS |
Ordinary words are translated even when they describe protocol things — "path" becomes
"ścieżka", "label" becomes "etykieta". The rule is whether the word is a value the device
uses or a name the user reads. Flood in a path field is a value; "Configured paths" is
a heading.
| CLI surfaces | everything the Console prints, its help screen, Usage: lines, command names |
| Log output | log lines and the level names DEBUG, INFO, WARNING, ERROR |
| Device/firmware fields | radio.rxgain, advert.interval and similar |
| Region and preset names | EU/UK (Narrow), USA/Canada (Recommended) — these are proper nouns |
You do translate the text around them. A sentence explaining a technical concept is translated even though the term inside it is not:
"settings.device.path_hash_desc": "Bytes per hop in routing paths. 1B = shortest path, more collisions."
"settings.device.path_hash_desc": "Bajtów na hop w ścieżkach routingu. 1B = najkrótsza ścieżka, więcej kolizji."
"hop" survives untranslated inside translated prose. That is the intended shape. Inflecting a term is fine — "hopów", "repeatera" — deleting it is not.
The canonical glossary list lives in scripts/i18n_check.py (GLOSSARY), which warns
when a translation drops one of these terms.
2. Adding a language
Name the file correctly
The filename is the language code, and it must be a two-letter code, optionally
followed by an uppercase region: hu.json, de.json, pt-BR.json, zh-CN.json.
A file that does not match that shape is ignored silently — no error, no entry in the
Language dropdown, nothing in the log. pt-br.json, PT-BR.json, hungarian.json and
hu_HU.json all fail this way. If your language does not appear after a refresh and the
⟳ button, check the filename first.
Get the file to work from
cp app/translations/en.json app/translations/hu.json # or work outside the repo
Then translate the values, never the keys:
{
"meta.language_name": "Magyar",
"meta.language_english_name": "Hungarian",
"meta.translator": "Your Name <you@example.com>",
"common.close": "Bezárás",
"chat.compose.input_ph": "Írj üzenetet..."
}
meta.language_name is what appears in the Settings dropdown, written in the language
itself. The other meta.* keys are optional.
Install it on your server
Drop the file into the translations folder inside your config directory — the same
volume that holds the database (/data inside the container, MC_CONFIG_DIR on the
host). With the stock docker-compose.yml that is:
mkdir -p ./data/translations
cp hu.json ./data/translations/
Putting the file in
app/translations/instead also works, but that folder is baked into the container image — it needsdocker compose up -d --build. Use it only when you are contributing the language back to the project. The config directory is the one that needs no rebuild and survives an update.
Refresh the browser. The language appears in Settings → Appearance → Language immediately; no restart is needed. If it does not show up (some network filesystems report file timestamps too coarsely), click the ⟳ button next to the dropdown.
A file in this folder overrides a built-in one of the same name, so you can also use
it to correct the shipped pl.json on your own server without touching the image.
Contribute it back
Open a pull request adding your file to app/translations/. Please run the checker
first (below) and mention which mc-webui version you translated against.
3. The file format
Flat keys, one JSON object, no nesting:
"settings.appearance.theme": "Motyw"
Key names describe where the string appears (settings.appearance.theme), and a suffix
describes what kind of string it is:
| Suffix | Meaning |
|---|---|
| (none) | visible text |
_title |
tooltip (title=) |
_ph |
input placeholder |
_aria |
screen-reader label |
_desc |
helper text under a control |
_btn |
button label |
Placeholders
{name} is substituted at runtime. Keep every placeholder — the checker reports a
mismatch as an error. You may reorder them freely:
"toast.contacts.deleted": "Deleted {name}"
"toast.contacts.deleted": "Usunięto kontakt {name}"
A { that is not followed by a word is left alone, so set {name} <value> is safe.
Plurals
A value may be an object of plural forms instead of a string. Use the categories your language actually needs — the app picks the right one via the browser's own CLDR rules:
"contacts.path.hops": { "one": "({count} hop)", "other": "({count} hops)" }
"contacts.path.hops": {
"one": "({count} hop)",
"few": "({count} hopy)",
"many": "({count} hopów)",
"other": "({count} hopa)"
}
{count} is always available. Valid categories: zero, one, two, few, many,
other. English needs one + other; Polish needs four.
Plurals in server-rendered text use a simpler built-in rule table that implements English and Polish exactly and falls back to
one/otherelsewhere. Client-rendered plurals — the large majority — always use the browser's full CLDR rules.
Markup
A few values contain HTML such as <b> or <a href="...">. Keep the tags intact and
translate the text between them. Do not add markup to a value that had none.
Missing keys
Anything you leave out falls back to English automatically. A partial translation is perfectly usable — ship it and fill in the rest over time.
4. Checking your work
python scripts/i18n_check.py # everything
python scripts/i18n_check.py --lang hu # one language
python scripts/i18n_check.py --missing hu # your worklist: untranslated keys + English text
--missing prints tab-separated key<TAB>English text, which pastes straight into a
spreadsheet.
The checker reports as errors: a placeholder you dropped or invented, a key that no longer exists, invalid JSON. As warnings: a glossary term that disappeared, and keys present in the catalog but unused in the code.
5. Security note
An installed catalog is trusted content. Values may contain HTML, and the app renders it — the same trust level as installing a plugin. Only install catalogs you have read or that came from a source you trust. Interpolated values (contact names, message text) are always escaped, so a catalog cannot be used to attack data flowing through it, and catalog text is never executed as code.
A malformed or unreadable catalog is logged and skipped; it cannot take the app down.