diff --git a/examples/asynchronous_telebot/middleware/i18n_middleware_example/keyboards.py b/examples/asynchronous_telebot/middleware/i18n_middleware_example/keyboards.py new file mode 100644 index 0000000..14d0473 --- /dev/null +++ b/examples/asynchronous_telebot/middleware/i18n_middleware_example/keyboards.py @@ -0,0 +1,34 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton + + +def languages_keyboard(): + return InlineKeyboardMarkup( + keyboard=[ + [ + InlineKeyboardButton(text="English", callback_data='en'), + InlineKeyboardButton(text="Русский", callback_data='ru'), + InlineKeyboardButton(text="O'zbekcha", callback_data='uz_Latn') + ] + ] + ) + + +def clicker_keyboard(_): + return InlineKeyboardMarkup( + keyboard=[ + [ + InlineKeyboardButton(text=_("click"), callback_data='click'), + ] + ] + ) + + +def menu_keyboard(_): + keyboard = ReplyKeyboardMarkup(resize_keyboard=True) + keyboard.add( + KeyboardButton(text=_("My user id")), + KeyboardButton(text=_("My user name")), + KeyboardButton(text=_("My first name")) + ) + + return keyboard diff --git a/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/en/LC_MESSAGES/messages.po b/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..2cf8eef --- /dev/null +++ b/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/en/LC_MESSAGES/messages.po @@ -0,0 +1,81 @@ +# English translations for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-02-19 18:37+0500\n" +"PO-Revision-Date: 2022-02-18 16:22+0500\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.1\n" + +#: keyboards.py:20 +msgid "click" +msgstr "" + +#: keyboards.py:29 +msgid "My user id" +msgstr "" + +#: keyboards.py:30 +msgid "My user name" +msgstr "" + +#: keyboards.py:31 +msgid "My first name" +msgstr "" + +#: main.py:97 +msgid "" +"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" +msgstr "" + +#: main.py:121 +msgid "Language has been changed" +msgstr "" + +#: main.py:130 main.py:150 +#, fuzzy +msgid "You have {number} click" +msgid_plural "You have {number} clicks" +msgstr[0] "" +msgstr[1] "" + +#: main.py:135 main.py:155 +msgid "" +"This is clicker.\n" +"\n" +msgstr "" + +#: main.py:163 +msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot." +msgstr "" + +#: main.py:203 +msgid "Seems you confused language" +msgstr "" + +#~ msgid "" +#~ "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" +#~ msgstr "" + diff --git a/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/ru/LC_MESSAGES/messages.po b/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/ru/LC_MESSAGES/messages.po new file mode 100644 index 0000000..6d330b0 --- /dev/null +++ b/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/ru/LC_MESSAGES/messages.po @@ -0,0 +1,82 @@ +# Russian translations for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-02-19 18:37+0500\n" +"PO-Revision-Date: 2022-02-18 16:22+0500\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.1\n" + +#: keyboards.py:20 +msgid "click" +msgstr "Клик" + +#: keyboards.py:29 +msgid "My user id" +msgstr "Мой user id" + +#: keyboards.py:30 +msgid "My user name" +msgstr "Мой user name" + +#: keyboards.py:31 +msgid "My first name" +msgstr "Мой first name" + +#: main.py:97 +msgid "" +"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" +msgstr "" +"Привет, {user_fist_name}!\n" +"Это пример мультиязычного бота.\n" +"Доступные команды:\n" +"\n" +"/lang - изменить язык\n" +"/plural - пример плюрализации\n" +"/menu - Пример текстового меню" + +#: main.py:121 +msgid "Language has been changed" +msgstr "Язык был сменён" + +#: main.py:130 main.py:150 +msgid "You have {number} click" +msgid_plural "You have {number} clicks" +msgstr[0] "У вас {number} клик" +msgstr[1] "У вас {number} клика" +msgstr[2] "У вас {number} кликов" + +#: main.py:135 main.py:155 +msgid "" +"This is clicker.\n" +"\n" +msgstr "" +"Это кликер.\n" +"\n" + +#: main.py:163 +msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot." +msgstr "Это пример ReplyKeyboardMarkup меню в мультиязычном боте." + +#: main.py:203 +msgid "Seems you confused language" +msgstr "Кажется, вы перепутали язык" + diff --git a/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/uz_Latn/LC_MESSAGES/messages.po b/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/uz_Latn/LC_MESSAGES/messages.po new file mode 100644 index 0000000..c5bd000 --- /dev/null +++ b/examples/asynchronous_telebot/middleware/i18n_middleware_example/locales/uz_Latn/LC_MESSAGES/messages.po @@ -0,0 +1,80 @@ +# Uzbek (Latin) translations for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-02-19 18:37+0500\n" +"PO-Revision-Date: 2022-02-18 16:22+0500\n" +"Last-Translator: FULL NAME \n" +"Language: uz_Latn\n" +"Language-Team: uz_Latn \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.1\n" + +#: keyboards.py:20 +msgid "click" +msgstr "clik" + +#: keyboards.py:29 +msgid "My user id" +msgstr "Mani user id" + +#: keyboards.py:30 +msgid "My user name" +msgstr "Mani user name" + +#: keyboards.py:31 +msgid "My first name" +msgstr "Mani first name" + +#: main.py:97 +msgid "" +"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" +msgstr "" +"Salom, {user_fist_name}!\n" +"Bu multilanguage bot misoli.\n" +"Mavjud buyruqlar:\n" +"\n" +"/lang - tilni ozgartirish\n" +"/plural - pluralizatsiya misoli\n" +"/menu - text menu misoli" + +#: main.py:121 +msgid "Language has been changed" +msgstr "Til ozgartirildi" + +#: main.py:130 main.py:150 +msgid "You have {number} click" +msgid_plural "You have {number} clicks" +msgstr[0] "Sizda {number}ta clik" +msgstr[1] "Sizda {number}ta clik" + +#: main.py:135 main.py:155 +msgid "" +"This is clicker.\n" +"\n" +msgstr "" +"Bu clicker.\n" +"\n" + +#: main.py:163 +msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot." +msgstr "Bu multilanguage bot da replykeyboardmarkup menyu misoli." + +#: main.py:203 +msgid "Seems you confused language" +msgstr "Tilni adashtirdiz" + diff --git a/examples/asynchronous_telebot/middleware/i18n_middleware_example/main.py b/examples/asynchronous_telebot/middleware/i18n_middleware_example/main.py new file mode 100644 index 0000000..2cee2c7 --- /dev/null +++ b/examples/asynchronous_telebot/middleware/i18n_middleware_example/main.py @@ -0,0 +1,208 @@ +""" +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 +""" + +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 telebot.asyncio_middlewares 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("1254795383:AAE7gbj1aas4lEDHB1eVuZZhSGPWcH1B5ds", 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.middlewares.append(i18n) + bot.add_custom_filter(TextMatchFilter()) + asyncio.run(bot.infinity_polling())