Plugin Internationalization
Plugins get their own isolated translation namespace using make_translator from horus_runtime.i18n. Each plugin calls this factory once at module load time and receives a tr callable bound exclusively to its own domain and locale directory.
Setup
Three lines of boilerplate in your plugin's top-level i18n.py (or equivalent):
from pathlib import Path
from horus_runtime.i18n import make_translator
tr = make_translator("my_plugin", Path(__file__).parent / "locale")
Then import and alias tr as _ in every module that has user-visible strings:
from my_plugin.i18n import tr as _
_("Task {name} started.", name=self.name)
_("Processed {n} file", "Processed {n} files", n=count)
make_translator calls gettext.translation with fallback=True, so an absent or unsupported locale returns the original English string without raising an exception.
Why Not Import horus-runtime's tr Directly?
horus-runtime's tr is bound to the horus_runtime gettext domain and its own locale directory. Strings from your plugin would never be found there. Each plugin must create its own translator with its own domain so that:
- Your
.po/.mofiles live inside your own package - Your strings are extracted and compiled independently from everyone else's
- Users can translate your plugin without touching horus-runtime's catalogs
Locale Directory Structure
Follow the standard gettext layout inside your package:
src/my_plugin/
i18n.py # make_translator call lives here
locale/
messages.pot # master template (auto-generated by pybabel)
es/
LC_MESSAGES/
my_plugin.po # Spanish translations
my_plugin.mo # compiled binary (generated by pybabel compile)
fr/
LC_MESSAGES/
my_plugin.po
my_plugin.mo
The domain string passed to make_translator ("my_plugin") must match the .po/.mo filename.
Babel Workflow
The workflow mirrors the one used for horus-runtime itself. Add a babel.cfg to your plugin repository:
[python: src/**.py]
Then manage translations with pybabel:
Extract strings
pybabel extract -F babel.cfg -o src/my_plugin/locale/messages.pot src/
Initialize a new language
pybabel init -i src/my_plugin/locale/messages.pot \
-d src/my_plugin/locale \
-D my_plugin \
-l es
Update existing catalogs after source changes
pybabel update -i src/my_plugin/locale/messages.pot \
-d src/my_plugin/locale \
-D my_plugin
Compile to binary
pybabel compile -d src/my_plugin/locale -D my_plugin
If you use the same Makefile targets as horus-runtime (babel-extract, babel-refresh, babel-check, babel-add), point them at your package's locale directory and domain.
Usage Patterns
All the same patterns as horus-runtime's tr apply:
from my_plugin.i18n import tr as _
# Simple string
message = _("Plugin initialized")
# Variable substitution
message = _("Connected to {host}:{port}", host=hostname, port=port)
# Pluralization
message = _("{n} item processed", "{n} items processed", n=count)
# Plural + extra variables
message = _("Uploaded {n} file to {bucket}",
"Uploaded {n} files to {bucket}",
n=count, bucket=bucket_name)
Packaging Locale Files
Make sure the compiled .mo files are included in your distribution. In pyproject.toml:
[tool.setuptools.package-data]
my_plugin = ["locale/**/*.mo"]
Without this, users will receive English strings regardless of their locale.