""" In this example you will learn how to adapt your bot to different languages Using built-in middleware I18N. You need to install babel package 'https://pypi.org/project/Babel/' Babel provides a command-line interface for working with message catalogs After installing babel package you have a script called 'pybabel' Too see all the commands open terminal and type 'pybabel --help' Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html' Create a directory 'locales' where our translations will be stored First we need to extract texts: pybabel extract -o locales/{domain_name}.pot --input-dirs . {domain_name}.pot - is the file where all translations are saved The name of this file should be the same as domain which you pass to I18N class In this example domain_name will be 'messages' For gettext (singular texts) we use '_' alias and it works perfect You may also you some alias for ngettext (plural texts) but you can face with a problem that your plural texts are not being extracted That is because by default 'pybabel extract' recognizes the following keywords: _, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_ To add your own keyword you can use '-k' flag In this example for 'ngettext' i will assign double underscore alias '__' Full command with pluralization support will look so: pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs . Then create directories with translations (get list of all locales: 'pybabel --list-locales'): pybabel init -i locales/{domain_name}.pot -d locales -l en pybabel init -i locales/{domain_name}.pot -d locales -l ru pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po After you translated all the texts you need to compile .po files: pybabel compile -d locales When you delete/update your texts you also need to update them in .po files: pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs . pybabel update -i locales/{domain_name}.pot -d locales - translate pybabel compile -d locales If you have any exceptions check: - you have installed babel - translations are ready, so you just compiled it - in the commands above you replaced {domain_name} to messages - you are writing commands from correct path in terminal """ import asyncio from typing import Union import keyboards from telebot import types from telebot.async_telebot import AsyncTeleBot from telebot.asyncio_filters import TextMatchFilter, TextFilter from i18n_base_midddleware import I18N from telebot.asyncio_storage.memory_storage import StateMemoryStorage class I18NMiddleware(I18N): def process_update_types(self) -> list: """ Here you need to return a list of update types which you want to be processed """ return ['message', 'callback_query'] async def get_user_language(self, obj: Union[types.Message, types.CallbackQuery]): """ This method is called when new update comes (only updates which you return in 'process_update_types' method) Returned language will be used in 'pre_process' method of parent class Returned language will be set to context language variable. If you need to get translation with user's actual language you don't have to pass it manually It will be automatically passed from context language value. However if you need some other language you can always pass it. """ user_id = obj.from_user.id if user_id not in users_lang: users_lang[user_id] = 'en' return users_lang[user_id] storage = StateMemoryStorage() bot = AsyncTeleBot("", state_storage=storage) i18n = I18NMiddleware(translations_path='locales', domain_name='messages') _ = i18n.gettext # for singular translations __ = i18n.ngettext # for plural translations # These are example storages, do not use it in a production development users_lang = {} users_clicks = {} @bot.message_handler(commands='start') async def start_handler(message: types.Message): text = _("Hello, {user_fist_name}!\n" "This is the example of multilanguage bot.\n" "Available commands:\n\n" "/lang - change your language\n" "/plural - pluralization example\n" "/menu - text menu example") # remember don't use f string for interpolation, use .format method instead text = text.format(user_fist_name=message.from_user.first_name) await bot.send_message(message.from_user.id, text) @bot.message_handler(commands='lang') async def change_language_handler(message: types.Message): await bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang", reply_markup=keyboards.languages_keyboard()) @bot.callback_query_handler(func=None, text=TextFilter(contains=['en', 'ru', 'uz_Latn'])) async def language_handler(call: types.CallbackQuery): lang = call.data users_lang[call.from_user.id] = lang # When you changed user language, you have to pass it manually beacause it is not changed in context await bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id) @bot.message_handler(commands='plural') async def pluralization_handler(message: types.Message): if not users_clicks.get(message.from_user.id): users_clicks[message.from_user.id] = 0 clicks = users_clicks[message.from_user.id] text = __( singular="You have {number} click", plural="You have {number} clicks", n=clicks ) text = _("This is clicker.\n\n") + text.format(number=clicks) await bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_)) @bot.callback_query_handler(func=None, text=TextFilter(equals='click')) async def click_handler(call: types.CallbackQuery): if not users_clicks.get(call.from_user.id): users_clicks[call.from_user.id] = 1 else: users_clicks[call.from_user.id] += 1 clicks = users_clicks[call.from_user.id] text = __( singular="You have {number} click", plural="You have {number} clicks", n=clicks ) text = _("This is clicker.\n\n") + text.format(number=clicks) await bot.edit_message_text(text, call.from_user.id, call.message.message_id, reply_markup=keyboards.clicker_keyboard(_)) @bot.message_handler(commands='menu') async def menu_handler(message: types.Message): text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.") await bot.send_message(message.chat.id, text, reply_markup=keyboards.menu_keyboard(_)) # For lazy tranlations # lazy gettext is used when you don't know user's locale # It can be used for example to handle text buttons in multilanguage bot # The actual translation will be delayed until update comes and context language is set l_ = i18n.lazy_gettext # Handlers below will handle text according to user's language @bot.message_handler(text=l_("My user id")) async def return_user_id(message: types.Message): await bot.send_message(message.chat.id, str(message.from_user.id)) @bot.message_handler(text=l_("My user name")) async def return_user_id(message: types.Message): username = message.from_user.username if not username: username = '-' await bot.send_message(message.chat.id, username) # You can make it case insensitive @bot.message_handler(text=TextFilter(equals=l_("My first name"), ignore_case=True)) async def return_user_id(message: types.Message): await bot.send_message(message.chat.id, message.from_user.first_name) all_menu_texts = [] for language in i18n.available_translations: for menu_text in ("My user id", "My user name", "My first name"): all_menu_texts.append(_(menu_text, language)) # When user confused language. (handles all menu buttons texts) @bot.message_handler(text=TextFilter(contains=all_menu_texts, ignore_case=True)) async def missed_message(message: types.Message): await bot.send_message(message.chat.id, _("Seems you confused language"), reply_markup=keyboards.menu_keyboard(_)) if __name__ == '__main__': bot.setup_middleware(i18n) bot.add_custom_filter(TextMatchFilter()) asyncio.run(bot.infinity_polling())