The async-zulip-bot-sdk includes a lightweight, JSON-based internationalization system that allows you to easily support multiple languages in your bots.
The i18n system:
"Hello {name}")BaseBot via the tr() method{name}, {count}, etc.)Translations are organized as follows:
project_root/
├── bot_sdk/
│ └── i18n/
│ ├── en.json # SDK defaults (English)
│ └── zh.json # SDK defaults (Chinese)
│
└── bots/
├── my_bot/
│ ├── __init__.py
│ ├── bot.yaml # Bot config with language: zh
│ └── i18n/ # Bot-specific translations (optional)
│ ├── en.json
│ └── zh.json
│
└── other_bot/
├── __init__.py
├── bot.yaml # language: en
└── i18n/
└── zh.json
Per-bot language is set in bot.yaml:
# bots/my_bot/bot.yaml
owner_user_id: 123
language: zh
role_levels:
user: 1
admin: 50
bot_owner: 200
settings: {}
"en", "zh", "fr"). Defaults to "en".When BaseBot initializes, it loads translations from:
<bot_dir>/i18n/{language}.json (bot-specific overrides)<bot_dir>/i18n/en.json (bot-specific fallback to English)bot_sdk/i18n/{language}.json (SDK defaults)bot_sdk/i18n/en.json (SDK defaults fallback to English)from bot_sdk import BaseBot, Message, CommandSpec, CommandArgument
class MyBot(BaseBot):
def register_commands(self):
# Descriptions can be translated at registration time
self.command_parser.register_spec(
CommandSpec(
name="hello",
description=self.tr("Greet a user"), # Uses i18n
args=[
CommandArgument(
name="name",
type=str,
required=True,
description=self.tr("The name to greet")
),
],
handler=self.handle_hello,
)
)
async def handle_hello(self, invocation, message, bot):
name = invocation.args.get("name")
# Translate user-facing messages
reply = self.tr("Hello {name}!", name=name)
await self.send_reply(message, reply)
async def on_message(self, message: Message) -> None:
# Translate any user-visible text
await self.send_reply(
message,
self.tr("Thank you for your message!")
)
class MyBot(BaseBot):
async def handle_command(self, invocation, message, bot):
try:
# Validate and process
result = self.do_something(invocation.args)
except ValueError as e:
# Error messages translated
await self.send_reply(
message,
"❌ " + self.tr("Invalid input: {error}", error=str(e))
)
return
# Success message translated
await self.send_reply(
message,
"✅ " + self.tr("Operation successful!")
)
Translations are simple key-value JSON files:
{
"Hello {name}!": "¡Hola {name}!",
"Greet a user": "Saludar a un usuario",
"The name to greet": "El nombre para saludar",
"Invalid input: {error}": "Entrada inválida: {error}",
"Operation successful!": "¡Operación exitosa!",
"Thank you for your message!": "¡Gracias por tu mensaje!"
}
"Hello {name}" help identify where values will be inserted.For bots/my_bot/i18n/es.json:
{
"Greet a user": "Saludar a un usuario",
"The name to greet": "El nombre para saludar",
"Hello {name}!": "¡Hola {name}!",
"Thank you for your message!": "¡Gracias por tu mensaje!"
}
The SDK provides default translations for built-in commands and UI strings:
English (bot_sdk/i18n/en.json):
Chinese (bot_sdk/i18n/zh.json):
You can override any of these in your bot’s own i18n/{language}.json file.
For advanced use cases, you can instantiate I18n directly:
from bot_sdk.i18n import I18n
# Load Chinese translations, searching multiple paths
i18n = I18n(
language="zh",
search_paths=[
"bots/my_bot/i18n", # Bot-specific translations
"bot_sdk/i18n" # SDK defaults
],
default_language="en" # Fallback to English
)
# Translate a string
greeting = i18n.translate("Hello {name}!", name="Alice")
print(greeting) # Output: 你好 Alice!
BaseBot uses a helper function to automatically locate and load translations:
from bot_sdk.i18n import build_i18n_for_bot
# Called automatically by BaseBot._init_i18n()
i18n = build_i18n_for_bot(
language="zh",
bot_module_name="bots.my_bot" # Automatically finds bot directory
)
This function:
<bot_dir>/i18n for translationsbot_sdk/i18nI18n instanceThe built-in !reload command (admin-only) reinitializes the i18n system without restarting the bot:
User: !reload
Bot: ✅ Configuration and translations reloaded.
This is useful when:
bot.yaml to change the language fieldCommandParser automatically receives a translator function from BaseBot:
# In BaseBot.__init__:
self.command_parser = CommandParser(
prefixes=self.command_prefixes,
enable_mentions=self.enable_mention_commands,
auto_help=self.auto_help_command,
translator=lambda s: self.tr(s), # Pass bot's tr method
)
This means:
Translate user-visible text only
Use consistent key naming
Provide fallbacks
Test multi-language support
language: en, language: zh, etc.!reload properly switches languagesOrganize bot-specific translations
bots/my_bot/i18n/ directory with en.json and other language filesfrom bot_sdk import BaseBot, Message, CommandSpec, CommandArgument, run_bot
class MultiLangBot(BaseBot):
def register_commands(self):
self.command_parser.register_spec(
CommandSpec(
name="fortune",
description=self.tr("Tell a fortune"),
handler=self.handle_fortune,
)
)
async def handle_fortune(self, invocation, message, bot):
fortunes = {
"en": "Today is a good day!",
"zh": "今天是个好日子!",
}
fortune = fortunes.get(self.language, "Today is a good day!")
await self.send_reply(message, fortune)
async def on_message(self, message: Message) -> None:
await self.send_reply(
message,
self.tr("Thanks for the message, {name}!", name=message.sender_full_name)
)
if __name__ == "__main__":
run_bot(MultiLangBot)
With bot.yaml set to language: zh, this bot will respond in Chinese. Use !reload to switch languages dynamically.
Translation not appearing?
Placeholders not substituting?
tr("Hello {name}", name="Alice") not tr("Hello {name}", user="Alice")Want to add a new language?
bot_sdk/i18n/xx.json or bots/my_bot/i18n/xx.json (where xx is the language code)language: xx in the bot’s bot.yaml!reload to load the new translations