Runtime Internationalization
gettext/Babel catalogs, runtime locale selection, and locale-exclusive command triggers.
Runtime Internationalization
Lingchu Bot uses Python gettext and Babel to manage runtime feedback text and command trigger words. This page covers the runtime side: catalog layout, locale selection, and the locale-exclusive trigger rule. For the development-time extraction / update / compile workflow, see Internationalization.
Catalog structure
Translation files live under src/plugins/nonebot_plugin_lingchu_bot/i18n:
src/plugins/nonebot_plugin_lingchu_bot/i18n/
├── babel.cfg
├── messages.pot
└── locales/
├── en_US/LC_MESSAGES/messages.po
├── en_US/LC_MESSAGES/messages.mo
├── zh_CN/LC_MESSAGES/messages.po
└── zh_CN/LC_MESSAGES/messages.mo| File | Role |
|---|---|
babel.cfg | Extraction config — [python: **.py] with UTF-8 encoding |
messages.pot | Extraction template, regenerated by pybabel extract |
messages.po | Editable translation source per locale |
messages.mo | Compiled binary catalog loaded by gettext at runtime |
The gettext domain is messages (constant DOMAIN in i18n/__init__.py), and the locales directory is resolved from Path(__file__).parent / "locales" (constant LOCALES_DIR). Only zh_CN and en_US ship today.
Runtime locale selection
Runtime code reads the lingchu_locale NoneBot configuration key. The recommended project-specific key in .env is LINGCHU_LOCALE:
LINGCHU_LOCALE=zh_CNget_configured_locale() in i18n/__init__.py is the single entry point. It reads the cached value from _read_configured_locale() (an lru_cache-backed reader that calls get_driver().config), normalizes the value, and returns it. normalize_locale() handles three cases:
en-USbecomesen_US(hyphens → underscores).en_US.UTF-8becomesen_US(encoding suffix stripped).- Empty or unset values fall back to the in-code default.
The in-code default is DEFAULT_LOCALE = "en_US" in i18n/__init__.py. The shipped .env.example sets LINGCHU_LOCALE=zh_CN as the recommended default for new deployments, so most installations run with zh_CN unless overridden.
get_translation() opens catalogs with fallback=True, so an untranslated message returns the original string instead of raising MissingMessageError and interrupting command handling.
Locale-exclusive command triggers
Group command trigger words are locale-exclusive. The same matcher registers either its Chinese trigger set or its English trigger set, never both. The selection is driven by _is_english_locale() in handle/qq/commands/triggers.py:
def _is_english_locale(locale: str | None = None) -> bool:
configured_locale = get_configured_locale() if locale is None else locale
return normalize_locale(configured_locale).lower().startswith("en")Each CommandTrigger carries parallel chinese / english primaries and chinese_aliases / english_aliases sets. primary_for(locale) and aliases_for(locale) return only the active language's values, so inactive-language aliases stay out of the matcher.
For example, the menu command registers either 菜单 (with aliases 帮助, 功能, 功能列表, 指令, 命令列表) or menu (with aliases help, commands), depending on the configured locale.
build_command_triggers() applies runtime overrides from command_trigger_overrides on top of _DEFAULT_COMMAND_TRIGGERS, then calls _validate_no_duplicates() to ensure no trigger string is owned by two command keys across both languages. A duplicate raises ValueError at import time.
_() and async translation helpers
The module exposes both synchronous and async translation helpers:
| Helper | Use |
|---|---|
_ / gettext | Synchronous singular translation |
ngettext | Synchronous plural translation |
_async / gettext_async | Async singular translation (loads catalog in a worker thread) |
ngettext_async | Async plural translation |
get_translation(locale) | Cached NullTranslations object |
get_translation_async(locale) | Same, loaded off the event loop |
warm_translation_cache(locales) | Prewarm catalogs during startup |
Async handlers should use _async, gettext_async, or ngettext_async so synchronous file reads do not block the NoneBot event loop. Startup calls warm_translation_cache() (see start/startup.py) to prewarm the configured locale plus the default locale before any handler runs.
Do not translate at import time
Synchronous _ / gettext is only suitable for synchronous paths. Do not translate defaults that may vary by locale at module import time — read and translate them while the handler is executing.
Development workflow
After changing translatable strings, run:
task i18ntask i18n extracts, updates, and compiles catalogs in order. For the underlying pybabel commands and the full development-time workflow, see Internationalization.
See also
For user-facing feedback text behavior and trigger language switching, see Commands and Troubleshooting.
Last updated on