Patching and Adding Translations
The heavy image lets you patch the Panel's own translations - reword a label, fix a phrasing you don't like, or fill in keys for a language - without forking the Panel or rebuilding the image yourself. You drop small JSON override files into a directory, trigger a rebuild, and the heavy image deep-merges your changes on top of the base translations the Panel ships.
This is the operator-level mechanism, not the extension one
This page is about overriding the base Panel's translations as an operator running the heavy image. If you're an extension author and want to ship translations with your extension, that's a different (and simpler) workflow - see Concepts → Translations. The two don't conflict: extensions declare their own keys, and this override mechanism patches whatever ends up in the final translation files.
This only works on the heavy image. If you're still on the regular image, start with Switching to the Heavy Image.
How It Works
When you switched to the heavy image you added a ./build/translations volume mount that maps to /app/translations inside the container:
volumes:
- ./build/translations:/app/translationsThe heavy image's entrypoint does three things:
- Copies the Panel's base translation files into
/app/translations/- one flat JSON file per language (en.json,de.json,es.json,fr.json, …). This runs on every boot and after every rebuild, so these top-level files are regenerated from the shipped Panel each time. They are there for you to read, as the reference for what keys exist. - Stages your changes, before the frontend is built. Any top-level
<lang>.jsonthat isn't one of the shipped filenames is staged as a new language, and every*.jsonin/app/translations/overrides/is deep-merged onto the base language with the same filename. Sooverrides/en.jsonmerges intoen.json,overrides/de.jsonintode.json, and so on. - Builds the frontend from that staged set. The result is compiled into the Panel binary, which is where the Panel serves translations from - so your changes only take effect through a rebuild (see Applying Your Changes).
Mapped back to your host, the override directory is:
./build/translations/overrides/Edit the overrides, never the top-level files
The top-level files for languages the Panel already ships (./build/translations/en.json, etc.) are overwritten from the base Panel on every boot, and edits to them are both wiped and ignored by the build. The overrides/ directory is the durable, upgrade-safe place to put your changes - because step 1 regenerates the base and step 2 reapplies your overrides on top, your patches survive Panel upgrades automatically (as long as the keys still exist).
The Merge
The merge is a recursive deep-merge:
- Objects are merged key-by-key, recursively. You only name the keys you want to change; everything else keeps the Panel's value.
- Strings, numbers, and arrays are replaced wholesale at the leaf.
This means an override file is small - it mirrors only the slice of the structure you're touching, not the whole file.
File Shape
Each language file has two top-level keys, items and translations, exactly like the extension translation files:
translations- regular strings, nested as deeply as the Panel nests them. The Panel's convention ispages.<section>.<page>.<element>with leaf categories likebutton,modal,form,toast,error. Anything in{braces}is an interpolation variable - keep it intact or the string breaks.items- pluralized count strings, each with the six CLDR plural categories (zero,one,two,few,many,other).
The base file you're patching against is the Panel's frontend/src/translations.ts (the generated JSON lives at ./build/translations/en.json on your host once the container has booted once - open it to find the exact key path you want to change).
Patching an Existing String
Say you want to reword the English account page title and tweak a button label. Find the keys in en.json, then create ./build/translations/overrides/en.json containing only those keys, nested to match:
{
"translations": {
"pages": {
"account": {
"home": {
"title": "My Dashboard"
}
}
},
"common": {
"button": {
"save": "Save changes"
}
}
}
}Everything else in en.json is left untouched - the merge only overwrites pages.account.home.title and common.button.save.
Filling In or Fixing a Translated Language
The exact same mechanism works for any language the Panel ships. To override a German string, create ./build/translations/overrides/de.json:
{
"translations": {
"common": {
"button": {
"save": "Speichern"
}
}
}
}If you're filling in a pluralized item, provide every plural category your language uses. The Unicode CLDR plural rules table is the authoritative reference for which categories a language needs.
{
"items": {
"server": {
"zero": "{count} Server",
"one": "{count} Server",
"two": "{count} Server",
"few": "{count} Server",
"many": "{count} Server",
"other": "{count} Server"
}
}
}You can patch any language the Panel already ships. To see what's available, look at the top-level <lang>.json files under ./build/translations/ after a rebuild, or hit GET /api/languages.
Adding a Brand-New Language
For a language the Panel doesn't ship yet, you don't use overrides/ - those merge into an existing file, and there's nothing to merge into. Instead, drop a top-level file straight into the translations volume:
./build/translations/<lang>.jsonThe easiest way to start is to copy the shipped English file and translate it in place:
cp ./build/translations/en.json ./build/translations/eo.json
# then translate the values in eo.jsonOn the next rebuild this file is compiled into the Panel binary alongside the shipped languages, so the new language is served at /translations/<lang>.json and listed by /api/languages, which is what populates the language picker in account settings. The picker labels it using the browser's own locale data, so a valid language code shows up under its proper name with nothing further to register. (The boot-time copy of the shipped defaults only writes over their own filenames - it leaves your extra file alone.)
Pick a filename the Panel doesn't already ship. A top-level file whose name matches a shipped language is treated as the regenerated base copy and ignored - to change a shipped language, use overrides/ instead.
You don't have to translate everything up front. Any key you leave out falls back to its English value, so a partial file is perfectly usable - users on that language just see English for whatever you haven't translated yet, and you can fill more in over time. Where your language uses extra plural forms, fill in the CLDR categories that apply (zero, one, two, few, many, other); the Unicode CLDR plural rules table is the reference.
overrides/ vs. a top-level file
A file in overrides/ patches a language - merged on top, so it can be a tiny partial. A file directly in ./build/translations/ is a language - it stands on its own (with English filling any gaps). Use the first to tweak a shipped language, the second to introduce a new one.
Applying Your Changes
Translations are compiled into the binary, so they take effect through a rebuild. After you add or edit a language file or an override, trigger one:
- From the admin UI: go to the extensions management page and click Rebuild. (Requires the
extensions.manageadmin permission.) - It also runs automatically as part of any extension install/uninstall rebuild.
- A
docker compose restartpicks the change up too - the entrypoint notices the translations differ from whatever the cached binary was built with and rebuilds on boot.
Rebuilds are cached by the combination of your installed extensions and your translation files, so a translations-only edit is enough to invalidate the cache and produce a real build. Once it finishes, reload the Panel and your strings are live.
The rebuild takes a while, and the Panel stays up
A rebuild compiles the frontend and the binary. The Panel keeps serving on the previous binary while that happens and switches over when it finishes, so this isn't downtime - but your strings won't change until it completes. Watch the extension build log if you want to follow along.
Reverting a change also needs a rebuild
Removing a language file or an override doesn't take effect until the next rebuild either. Because the cache key follows your translation files, deleting them usually returns you to a binary that was already built and cached, which makes that particular rebuild fast or instant.
Caveats
- Override existing keys, don't invent new ones. The override is merged into the base file, but the Panel's frontend only reads keys it knows about. Adding a key the Panel never references does nothing; patch keys that already exist.
- Keep interpolation variables intact. If the original string is
Showing {start} to {end} of {total} results., your override has to keep{start},{end}, and{total}- dropping or renaming them breaks the rendered string. - Patches follow the base, not the other way around. Because the base is regenerated every rebuild, if a Panel upgrade renames or removes a key, your override for the old key simply has nothing to merge into and silently stops applying. After a major upgrade, skim the diff in
./build/translations/en.jsonif a patched string reverts. - Valid JSON only. A malformed override file is logged and skipped during the rebuild rather than applied - check the extension build log if a change doesn't take.