Internationalization (i18n) System
ErisPulse v2.5.0 includes full internationalization support. Both the framework core and CLI interface can automatically switch display text according to your system language, and external modules can also register their own translations.
Supported Languages
| Language | Code | Description |
|---|---|---|
| Simplified Chinese | zh-CN |
Default language (native framework language) |
| Traditional Chinese | zh-TW |
Traditional Chinese (Hong Kong/Macau/Taiwan) |
| English | en |
English (general fallback language) |
| 日本語 | ja |
Japanese |
| Русский | ru |
Russian |
Quick Experience
Switch via Environment Variable
# Windows PowerShell
$env:ERISPULSE_LANG = "en"
epsdk run
# macOS / Linux
ERISPULSE_LANG=ja epsdk run
Switch via Configuration File
Add to config/config.toml:
[ErisPulse.i18n]
language = "zh-TW"
Set to "auto" (default) to automatically detect system language.
Switch Manually in Code
from ErisPulse import i18n
# Manually set language
i18n.set_language("en")
print(i18n.get_language()) # "en"
# Reset to auto-detect
i18n.reset_language()
Language Detection Mechanism
The framework detects user language with the following priority:
- Environment variable
ERISPULSE_LANG— Highest priority, for testing and temporary switching - Windows API —
GetUserDefaultLocaleName(Windows only, not affected by tools like Git Bash that overrideLANG) - Environment variables —
LANGUAGE>LC_ALL>LC_MESSAGES>LANG(Unix/macOS standard) - System Locale —
locale.getlocale()/locale.getdefaultlocale() - Fallback — en (English)
Nearest Mapping Principle
When detected language does not match exactly, map to the nearest supported language:
zh-TW,zh-HK,zh-MO,zh-Hant→ Traditional Chinese- All other
zh-*(e.g.zh-CN,zh-SG) → Simplified Chinese en-US,en-GB,en-AUetc. → Englishja-JP→ Japaneseru-RU→ Russian- Other unrecognized languages → Simplified Chinese (fallback)
Using i18n in Modules
You can register translation text for your own modules to make them support multiple languages.
Recommended Approach: Declare Translation Keys via I18nClass (v2.7.0+)
From v2.7.0, modules/adapters can declare translation keys by nesting the I18nClass class, similar to declaring a ConfigClass. The framework will automatically register all declared translation keys without requiring manual i18n.register() calls.
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BaseConfig, BaseI18n, BaseModule, I18nKey
class MyModule(BaseModule):
# Configuration class (optional)
@dataclass
class ConfigClass(BaseConfig):
welcome_msg: str = field(
default="欢迎",
metadata={
# Reference i18n key mymodule.welcome_msg here
"description": {"i18n": "mymodule.welcome_msg", "default": "Welcome message"},
},
)
# Translation key collection class (optional)
# Declared keys will be automatically registered by the framework, with higher priority than default configuration generated from ConfigClass
class I18nClass(BaseI18n):
# Property names are automatically concatenated into full key paths: <module name>.<property name>
welcome_msg: I18nKey = I18nKey(
default="Welcome Message", # Language-agnostic fallback, not registered to any language
zh_CN="欢迎消息",
en="Welcome Message",
ja="ウェルカムメッセージ",
ru="Приветственное сообщение",
zh_TW="歡迎訊息",
)
# Other business-related translation keys
hello: I18nKey = I18nKey(
default="Hello, {name}!",
zh_CN="你好,{name}!",
zh_TW="你好,{name}!",
en="Hello, {name}!",
ja="こんにちは、{name}!",
ru="Привет, {name}!",
)
# You can also explicitly specify the full key path (not using property name concatenation)
custom: I18nKey = I18nKey(
key="mymodule.deep.nested.key",
default="Default text",
zh_CN="默认文本",
zh_TW="預設文本",
en="Default text",
ja="デフォルトテキスト",
ru="Текст по умолчанию",
)
Why is I18nClass Recommended?
| Scenario | Manual i18n.register() | I18nClass Declarative |
|---|---|---|
| Configuration description referencing i18n keys | Need manual registration, and must be done before configuration generation | Framework automatically registers before configuration generation |
| Multi-language translation declaration | Scattered in various on_load() methods | Centralized in class, clearly visible |
| Key naming consistency | Prone to spelling errors | Property names as key suffixes, IDE can auto-complete |
| Unloading cleanup | Need manual unregister_domain() | Framework uses unified domain registration |
I18nClass Key Path Rules
- Default: Use
<module registration name>.<property name>as the full key path- Example: Module name is
MyModule, propertywelcome→ key pathMyModule.welcome
- Example: Module name is
- Explicit: Use the
I18nKey(key="...")parameter to specify any dot-separated path- Suitable for deeply nested key names (e.g.
mymodule.config.basic.token)
- Suitable for deeply nested key names (e.g.
Using in Adapters
Adapters also support I18nClass, with identical usage:
from ErisPulse.Core import BaseAdapter
from ErisPulse.Core.Bases import BaseConfig, BaseI18n, I18nKey
class MyAdapter(BaseAdapter):
@dataclass
class ConfigClass(BaseConfig):
endpoint: str = field(
default="",
metadata={
# Configuration description references adapter.MyAdapter.endpoint key
"description": {"i18n": "MyAdapter.endpoint", "default": "API address"},
},
)
class I18nClass(BaseI18n):
# Central declaration of configuration description referenced keys and other business keys' multilingual translations
endpoint: I18nKey = I18nKey(
default="API Endpoint",
zh_CN="API address",
zh_TW="API address",
en="API Endpoint",
ja="API address",
ru="API address",
)
The I18nClass of adapters is automatically registered during the __init__ phase (before configuration template generation), ensuring that i18n keys referenced in configuration descriptions are available.
Manual Registration of Custom Translations (Old Approach)
If you do not use I18nClass, you can directly call i18n.register() to register translation text.
from ErisPulse import i18n
# Register Chinese translations
i18n.register("zh-CN", {
"my_module.welcome": "Welcome to my module!",
"my_module.goodbye": "Goodbye!",
"my_module.hello": "Hello, {name}!",
}, domain="my_module")
# Register English translations
i18n.register("en", {
"my_module.welcome": "Welcome to my module!",
"my_module.goodbye": "Goodbye!",
"my_module.hello": "Hello, {name}!",
}, domain="my_module")
Using Translations
from ErisPulse import i18n
# Simple translation
i18n.t("my_module.welcome") # Automatically uses current language
# With formatted parameters
i18n.t("my_module.hello", name="Alice")
# Specify default value (returns when translation key does not exist)
i18n.t("my_module.unknown_key", default="Default text")
Using in Module Classes
from dataclasses import dataclass, field
from ErisPulse import i18n
from ErisPulse.Core.Bases import BaseConfig, BaseModule
@dataclass
class MyModuleConfig(BaseConfig):
welcome_msg: str = field(
default="欢迎",
metadata={
"description": {"i18n": "my_module.welcome_msg", "default": "Welcome message"},
"ui": {"widget": "text", "group": "basic", "order": 1},
},
)
class MyModule(BaseModule):
ConfigClass = MyModuleConfig
async def on_load(self, event):
# Real-time configuration access (reflects latest value on each access)
self.logger.info(self.cfg.welcome_msg)
self.logger.info(i18n.t("my_module.welcome"))
@command("hello")
async def hello_handler(self, event):
name = event.get_user_nickname() or "friend"
await event.reply(i18n.t("my_module.hello", name=name))
async def on_unload(self, event):
pass
Unregistering Translations
# Unregister all translations in a domain
i18n.unregister_domain("my_module")
Multi-language Configuration Fields
From v2.5.2, configuration Schema fully supports i18n. All user-visible text fields can reference i18n keys, and WebUI and other consumers will automatically resolve them into corresponding text based on the current language.
Supported i18n Fields
| Field | Location | Description |
|---|---|---|
description |
field metadata | Field description |
options[].label |
ui.options |
Select control option labels |
placeholder |
ui.placeholder |
Input box placeholder |
group_labels |
_schema_meta |
Group display names (Dashboard partition titles) |
All use the {"i18n": "key", "default": "text"} format. Pure strings are passed through as-is (for backward compatibility).
Declaring i18n Fields
All user-visible text fields support i18n:
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BaseConfig
@dataclass
class MyAdapterConfig(BaseConfig):
# description i18n
token: str = field(
default="",
metadata={
"description": {"i18n": "my_adapter.token", "default": "Platform Token"},
"required": True,
"secret": True,
"ui": {
"widget": "password",
"group": "basic",
"order": 1,
# placeholder i18n
"placeholder": {"i18n": "my_adapter.token.ph", "default": "Enter Token"},
},
},
)
# options label i18n
mode: str = field(
default="a",
metadata={
"description": {"i18n": "my_adapter.mode", "default": "Operation mode"},
"ui": {
"widget": "select",
"group": "basic",
"order": 2,
"options": [
{"label": {"i18n": "my_adapter.mode.a", "default": "Mode A"}, "value": "a"},
{"label": {"i18n": "my_adapter.mode.b", "default": "Mode B"}, "value": "b"},
],
},
},
)
# group_labels i18n (group display names)
_schema_meta = {
"group_labels": {
"basic": {"i18n": "my_adapter.group.basic", "default": "Basic Settings"},
}
}
default is the fallback text—shown when translation is not registered or lookup fails.
Secret Masking and Configuration Validation
Fields marked as "secret": True will automatically receive masking protection (from v2.7.0):
- Template generation masking: When
dataclass_to_toml_with_comments()generates configuration templates, secret fields' real values are not written to the file (displaying empty placeholders), preventing sensitive information from being written to disk - General masking utility:
redact_secret(value)replaces non-empty values with***, returning empty values as-is, suitable for scenarios like logging output
from ErisPulse.Core.Bases.config_schema import redact_secret
redact_secret("sk-xxxxxx") # '***'
redact_secret("") # ''
Configuration validation (validate_config()) supports additional checks beyond required non-empty checks (from v2.7.0):
| Validation | Metadata | Example |
|---|---|---|
| Type matching | Field declaration type | int field passed a string raises an error |
| Enum constraint | ui.options or top-level options |
Value must be among allowed options |
| Numeric range | Top-level min / max |
metadata={"min": 1, "max": 65535} |
from ErisPulse.Core.Bases.config_schema import validate_config
@dataclass
class C(BaseConfig):
mode: str = field(default="a", metadata={"ui": {"widget": "select", "options": ["a", "b"]}})
port: int = field(default=80, metadata={"min": 1, "max": 65535})
errors = validate_config(C(mode="x", port=70000)) # Two errors: enum + range
Registering Configuration Translations
Configuration field i18n keys and regular translation keys are registered the same way using i18n.register():
from ErisPulse import i18n
# Register Chinese (same as default, but can differ)
i18n.register("zh-CN", {
"my_adapter.token": "Platform Token",
}, domain="my_adapter")
# Register English
i18n.register("en", {
"my_adapter.token": "Platform Token",
}, domain="my_adapter")
Recommended approach: Use
I18nClassto declare translation keys, and the framework will automatically register them (see the "Recommended approach" section above), eliminating the need to manually calli18n.register()orregister_config_i18n().
A convenient function register_config_i18n() is also provided, which automatically extracts keys from the configuration class and registers them:
from ErisPulse.Core.Bases.config_schema import register_config_i18n
# Automatically extract description.default as zh-CN translation
register_config_i18n(MyAdapterConfig, "zh-CN")
# Manually provide English translation
register_config_i18n(MyAdapterConfig, "en", {
"my_adapter.token": "Platform Token",
})
How WebUI Consumes
get_config_schema() returns a schema where the i18n dictionary is passed through as-is. The WebUI frontend can use i18n.t() to resolve based on the current language.
If you need the server to directly resolve into strings (e.g., for frontends that do not support i18n), use resolve_config_schema(), which resolves description, options[].label, placeholder, and group_labels into the current language's text:
from ErisPulse.Core.Bases.config_schema import resolve_config_schema
# All i18n fields are resolved into the current language's string
schema = resolve_config_schema(MyAdapterConfig)
print(schema["fields"]["token"]["description"]) # "Platform Token" or "Platform Token"
print(schema["fields"]["token"]["placeholder"]) # "Enter Token" or "Enter Token"
print(schema["fields"]["mode"]["options"][0]["label"]) # "Mode A" or "Mode A"
print(schema["group_labels"]["basic"]) # "Basic Settings" or "Basic"
BaseConfig,BotAccountConfig,register_config_i18n(),resolve_config_schema()and other types and utility functions are actually defined inErisPulse.Core.Bases.config_schema.ErisPulse.runtime.config_schemais retained as a compatibility shim, recommended to import uniformly fromErisPulse.Core.Bases(except for i18n translation key related types, which are located inErisPulse.Core.Bases.i18n_schema).
API Reference
I18nManager
Core Methods
| Method | Description |
|---|---|
t(key, default=None, **kwargs) |
Get translated text (gettext() is an alias) |
set_language(lang) |
Manually set language |
get_language() |
Get current language |
reset_language() |
Reset to auto-detection (and re-detect environment) |
get_supported_languages() |
Get list of all supported languages |
has_translation(key, lang=None) |
Check if translation key exists |
register(lang, translations, domain) |
Register custom translations |
unregister_domain(domain) |
Unload all translations in specified domain |
reload() |
Reload built-in translations and re-detect language |
t() Method Details
def t(self, key, /, default=None, **kwargs):
key— Translation key (positional argument only, does not conflict with**kwargs'skey=)default— Default value returned when translation does not exist, default isNone(returns key name itself)**kwargs— Formatting parameters, used to fill placeholders in translation values
Example:
# Translation definition: "greeting": "你好,{name}!欢迎来到{place}。"
i18n.t("greeting", name="Alice", place="ErisPulse")
# Returns: "你好,Alice!欢迎来到ErisPulse。"
BaseI18n / I18nKey (Declarative Translation Keys)
Starting from v2.7.0, ErisPulse.Core.Bases provides a translation key declaration tool based on class attributes (recommended to import uniformly from ErisPulse.Core.Bases):
I18nKey.defaultis a language-agnostic fallback text and is not registered to any language. To make translations effective, at least one language parameter must be explicitly passed (zh_CN=/en=/ja=etc.). This allows developers from various countries to freely use their native language to fill indefault, with no assumptions made by the framework.
| Name | Description |
|---|---|
I18nKey(default, *, key=None, zh_CN, zh_TW, en, ja, ru) |
Declaration of a single translation key, default is a language-agnostic fallback |
BaseI18n |
Translation key collection base class (naming aligned with BaseConfig), sub-classes declare multiple I18nKey via class attributes |
BaseI18n.register(prefix="", domain="app") |
Class method: register all declared keys to the i18n system |
key |
Alias for I18nKey (more concise writing) |
Usage example:
from ErisPulse.Core.Bases import BaseI18n, key
class MyKeys(BaseI18n):
# Concise alias writing
hello = key(
default="Hello",
zh_CN="你好",
zh_TW="你好",
en="Hello",
ja="こんにちは",
ru="Привет",
)
bye = key(
default="Bye",
zh_CN="再见",
zh_TW="再見",
en="Bye",
ja="さようなら",
ru="До свидания",
)
# Standalone use (manual registration)
MyKeys.register(prefix="myapp.", domain="myapp")
Accessing from SDK Instance
from ErisPulse import sdk
# sdk.i18n is the same object as directly imported i18n
sdk.i18n.set_language("en")
print(sdk.i18n.t("core.sdk.init.starting"))
Runtime Configuration
Reading i18n Configuration via Configuration API
from ErisPulse.Core.Bases import I18nConfig
from ErisPulse.runtime import get_i18n_config
config = get_i18n_config()
print(config["language"]) # "auto" or specific language code
# I18nConfig is a dataclass, suitable for generating configuration templates
schema = I18nConfig.__dataclass_fields__
Configuration Item Description
In the [ErisPulse.i18n] section of config/config.toml:
[ErisPulse.i18n]
# Display language, possible values:
# - "auto" — Auto-detect system language (default)
# - "zh-CN" — Simplified Chinese
# - "zh-TW" — Traditional Chinese
# - "en" — English
# - "ja" — Japanese
# - "ru" — Russian
language = "auto"
Best Practices
Translation Key Naming
It is recommended to use dot-separated namespace format:
<module name>.<category>.<description>
For example: my_module.command.hello_desc, core.adapter.start_failed
Multi-language Coverage
There is no need to provide translations for all languages at once; missing languages will automatically fall back to English, and if English is also missing, the key name itself will be displayed.
Dynamic Content
For dynamically generated content (such as usernames, quantities, etc.), use {placeholder} formatting:
# Translation definition
"user_count": "Current online users: {count} people"
# Usage
i18n.t("user_count", count=len(users))
Log Messages
If your module uses the framework's Logger, these messages will also automatically use the current language:
self.logger.info(i18n.t("my_module.startup"))
Relationship with CLI i18n
The CLI has an independent internationalization module (ErisPulse.CLI.i18n), which is completely decoupled from the framework core's internationalization module.
- Core i18n — Used by framework core modules, external modules can register translations
- CLI i18n — Used internally by the command-line interface, does not share translation data with Core
This design ensures that changes to CLI translations do not affect the stability of the framework core.