Files
dgtlmoon 050172129e
Build and push containers / metadata (push) Canceled after 0s
Build and push containers / build-push-containers (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Canceled after 0s
ChangeDetection.io App Test / lint-code (push) Canceled after 0s
ChangeDetection.io App Test / lint-translations (push) Canceled after 0s
ChangeDetection.io App Test / lint-template-i18n (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-11 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-12 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-13 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-14 (push) Canceled after 0s
Translations - support for translation overlays/local translations of existing strings (#4365)
* Translations - support for translation overlays/local translations of existing strings

* Dont forget translations_overlay for pip
2026-09-03 11:15:46 +02:00
..
…
…

Translation overlay

An optional second catalog layer that lets a deployment reword individual strings without forking any template or Python source.

It exists because rewording a string normally means editing the _() call at its call site, and that edit conflicts on every upstream merge, forever. The overlay moves the reworded text out of the source tree and into a catalog keyed on the upstream msgid, so the call site keeps upstream's string verbatim and carries no diff at all.

Typical uses: white-labelling, or suppressing instructions that don't apply to how you run the app — e.g. telling users to set WEBDRIVER_URL when the operator, not the user, controls that.

How it works

BABEL_TRANSLATION_DIRECTORIES is a ; separated list. For each locale, Flask-Babel loads one catalog per directory and merges them in order (Domain.get_translations → babel Translations.merge → dict.update), so a later directory overrides an earlier one per message, not per file.

changedetectionio/translations/          <- base, upstream
changedetectionio/translations_overlay/  <- this layer, merged on top

So for German, translations/de and translations_overlay/de become a single catalog: the msgids listed in the overlay take the overlay's text, and the other ~580 keep their upstream German. A language with no overlay catalog is completely unaffected, as is a msgid the overlay doesn't mention.

Overriding English works too. The en_GB base catalog has every msgstr empty, so English currently renders straight from the msgids; an overlay en_GB entry fills that gap.

Wiring is in changedetectionio/flask_app.py. The directory is picked up only if it exists, and TRANSLATION_OVERLAY_DIR can point somewhere else (e.g. a path mounted into a container). No directory, or a directory with no overrides, means no behaviour change.

The failure mode this needs guarding

An override matches on the exact upstream msgid. When upstream edits that string — even fixing a typo — the override stops matching and the string silently reverts to upstream wording. It fails soft, so nothing tells you.

manage.py check turns that into a hard failure, and tests/test_translation_overlay.py runs it in CI. It also catches three other quiet ways an override does nothing:

Problem Why it bites
msgid no longer in messages.pot upstream reworded it; your override is dead
empty msgstr the compiler drops empty entries, so it silently does nothing
Plural-Forms differs from base Flask-Babel copies plural from the last catalog that has one, so a wrong header here breaks plurals for every string in that language
.mo stale or missing the .mo is what's loaded; .po edits alone have no effect

Workflow

# create a catalog for a language (inherits Plural-Forms from the base catalog)
python changedetectionio/translations_overlay/manage.py add de

# ... add msgid/msgstr pairs, copying the msgid verbatim from translations/messages.pot ...

python changedetectionio/translations_overlay/manage.py compile   # .po -> .mo
python changedetectionio/translations_overlay/manage.py check     # validate (also runs in CI)

Both .po and .mo are committed, matching how the base catalogs are handled.

Overriding a string in English only is fine — other languages keep their upstream translation of the original wording. If the reworded English changes the meaning rather than the phrasing, add the matching override per language, otherwise the translations will drift from what English now says.

Writing the override text itself follows the same rules as any other catalog entry: see ../translations/README.md, especially "do not fragment msgids".