Skip to content

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.

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 code reads the lingchu_locale NoneBot configuration key. The recommended project-specific key in .env is LINGCHU_LOCALE:

Terminal window
LINGCHU_LOCALE=zh_CN

get_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-US becomes en_US (hyphens → underscores).
  • en_US.UTF-8 becomes en_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.

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.

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.

After changing translatable strings, run:

Terminal window
task i18n

task i18n extracts, updates, and compiles catalogs in order. For the underlying pybabel commands and the full development-time workflow, see Internationalization.