Lingchu Bot documentation is now live — check it out!
Lingchu Bot

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
FileRole
babel.cfgExtraction config — [python: **.py] with UTF-8 encoding
messages.potExtraction template, regenerated by pybabel extract
messages.poEditable translation source per locale
messages.moCompiled 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_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.

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:

HelperUse
_ / gettextSynchronous singular translation
ngettextSynchronous plural translation
_async / gettext_asyncAsync singular translation (loads catalog in a worker thread)
ngettext_asyncAsync 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 i18n

task 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

On this page