From 3a5db47c1b08659d3d414413318a1dab6027ee07 Mon Sep 17 00:00:00 2001 From: abdullaev388 Date: Sat, 23 Apr 2022 10:44:35 +0500 Subject: [PATCH 1/5] removed old class based i18n middleware --- examples/i18n_class_example/i18n_class.py | 66 -------- examples/i18n_class_example/keyboards.py | 23 --- .../locales/en/LC_MESSAGES/messages.po | 51 ------ .../locales/ru/LC_MESSAGES/messages.po | 60 ------- .../locales/uz_Latn/LC_MESSAGES/messages.po | 58 ------- examples/i18n_class_example/main.py | 153 ------------------ 6 files changed, 411 deletions(-) delete mode 100644 examples/i18n_class_example/i18n_class.py delete mode 100644 examples/i18n_class_example/keyboards.py delete mode 100644 examples/i18n_class_example/locales/en/LC_MESSAGES/messages.po delete mode 100644 examples/i18n_class_example/locales/ru/LC_MESSAGES/messages.po delete mode 100644 examples/i18n_class_example/locales/uz_Latn/LC_MESSAGES/messages.po delete mode 100644 examples/i18n_class_example/main.py diff --git a/examples/i18n_class_example/i18n_class.py b/examples/i18n_class_example/i18n_class.py deleted file mode 100644 index aa02612..0000000 --- a/examples/i18n_class_example/i18n_class.py +++ /dev/null @@ -1,66 +0,0 @@ -import gettext -import os - - -class I18N: - """ - This class provides high-level tool for internationalization - It is based on gettext util. - """ - - def __init__(self, translations_path, domain_name: str): - self.path = translations_path - self.domain = domain_name - self.translations = self.find_translations() - - @property - def available_translations(self): - return list(self.translations) - - def gettext(self, text: str, lang: str = None): - """ - Singular translations - """ - if not lang or lang not in self.translations: - return text - - translator = self.translations[lang] - return translator.gettext(text) - - def ngettext(self, singular: str, plural: str, lang: str = None, n=1): - """ - Plural translations - """ - if not lang or lang not in self.translations: - if n == 1: - return singular - return plural - - translator = self.translations[lang] - return translator.ngettext(singular, plural, n) - - def find_translations(self): - """ - Looks for translations with passed 'domain' in passed 'path' - """ - if not os.path.exists(self.path): - raise RuntimeError(f"Translations directory by path: {self.path!r} was not found") - - result = {} - - for name in os.listdir(self.path): - translations_path = os.path.join(self.path, name, 'LC_MESSAGES') - - if not os.path.isdir(translations_path): - continue - - po_file = os.path.join(translations_path, self.domain + '.po') - mo_file = po_file[:-2] + 'mo' - - if os.path.isfile(po_file) and not os.path.isfile(mo_file): - raise FileNotFoundError(f"Translations for: {name!r} were not compiled!") - - with open(mo_file, 'rb') as file: - result[name] = gettext.GNUTranslations(file) - - return result diff --git a/examples/i18n_class_example/keyboards.py b/examples/i18n_class_example/keyboards.py deleted file mode 100644 index 4a94268..0000000 --- a/examples/i18n_class_example/keyboards.py +++ /dev/null @@ -1,23 +0,0 @@ -from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton - - -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(_, lang): - return InlineKeyboardMarkup( - keyboard=[ - [ - InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'), - ] - ] - ) diff --git a/examples/i18n_class_example/locales/en/LC_MESSAGES/messages.po b/examples/i18n_class_example/locales/en/LC_MESSAGES/messages.po deleted file mode 100644 index fc6565a..0000000 --- a/examples/i18n_class_example/locales/en/LC_MESSAGES/messages.po +++ /dev/null @@ -1,51 +0,0 @@ -# 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-18 17:54+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 "" - -#: main.py:78 -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 "" - -#: main.py:102 -msgid "Language has been changed" -msgstr "" - -#: main.py:114 -#, fuzzy -msgid "You have {number} click" -msgid_plural "You have {number} clicks" -msgstr[0] "" -msgstr[1] "" - -#: main.py:120 -msgid "" -"This is clicker.\n" -"\n" -msgstr "" - diff --git a/examples/i18n_class_example/locales/ru/LC_MESSAGES/messages.po b/examples/i18n_class_example/locales/ru/LC_MESSAGES/messages.po deleted file mode 100644 index 1733ef1..0000000 --- a/examples/i18n_class_example/locales/ru/LC_MESSAGES/messages.po +++ /dev/null @@ -1,60 +0,0 @@ -# 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-18 17:54+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 "Клик" - -#: main.py:78 -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 "" -"Привет, {user_fist_name}!\n" -"Это пример мультиязычного бота.\n" -"Доступные команды:\n" -"\n" -"/lang - изменить язык\n" -"/plural - пример плюрализации" - -#: main.py:102 -msgid "Language has been changed" -msgstr "Язык был сменён" - -#: main.py:114 -msgid "You have {number} click" -msgid_plural "You have {number} clicks" -msgstr[0] "У вас {number} клик" -msgstr[1] "У вас {number} клика" -msgstr[2] "У вас {number} кликов" - -#: main.py:120 -msgid "" -"This is clicker.\n" -"\n" -msgstr "" -"Это кликер.\n" -"\n" - diff --git a/examples/i18n_class_example/locales/uz_Latn/LC_MESSAGES/messages.po b/examples/i18n_class_example/locales/uz_Latn/LC_MESSAGES/messages.po deleted file mode 100644 index e694b78..0000000 --- a/examples/i18n_class_example/locales/uz_Latn/LC_MESSAGES/messages.po +++ /dev/null @@ -1,58 +0,0 @@ -# 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-18 17:54+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" - -#: main.py:78 -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 "" -"Salom, {user_fist_name}!\n" -"Bu multilanguage bot misoli.\n" -"Mavjud buyruqlar:\n" -"\n" -"/lang - tilni ozgartirish\n" -"/plural - pluralizatsiya misoli" - -#: main.py:102 -msgid "Language has been changed" -msgstr "Til ozgartirildi" - -#: main.py:114 -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:120 -msgid "" -"This is clicker.\n" -"\n" -msgstr "" -"Bu clicker.\n" -"\n" - diff --git a/examples/i18n_class_example/main.py b/examples/i18n_class_example/main.py deleted file mode 100644 index 23794f2..0000000 --- a/examples/i18n_class_example/main.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -In this example you will learn how to adapt your bot to different languages -Using built-in class 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 -""" - -from functools import wraps -import keyboards -from telebot import TeleBot, types, custom_filters -from telebot.storage.memory_storage import StateMemoryStorage -from i18n_class import I18N - -storage = StateMemoryStorage() -bot = TeleBot("", state_storage=storage) - -i18n = I18N(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 = {} - - -def get_user_language(func): - """ - This decorator will pass to your handler current user's language - """ - - @wraps(func) - def inner(*args, **kwargs): - obj = args[0] - kwargs.update(lang=users_lang.get(obj.from_user.id, 'en')) - return func(*args, **kwargs) - - return inner - - -@bot.message_handler(commands='start') -@get_user_language -def start_handler(message: types.Message, lang): - 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", lang=lang) - - # remember don't use f string for interpolation, use .format method instead - text = text.format(user_fist_name=message.from_user.first_name) - bot.send_message(message.from_user.id, text) - - -@bot.message_handler(commands='lang') -def change_language_handler(message: types.Message): - bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang", - reply_markup=keyboards.languages_keyboard()) - - -@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(contains=['en', 'ru', 'uz_Latn'])) -def language_handler(call: types.CallbackQuery): - lang = call.data - users_lang[call.from_user.id] = lang - - bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id) - bot.delete_state(call.from_user.id) - - -@bot.message_handler(commands='plural') -@get_user_language -def pluralization_handler(message: types.Message, lang): - 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, - lang=lang - ) - text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks) - bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_, lang)) - - -@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click')) -@get_user_language -def click_handler(call: types.CallbackQuery, lang): - 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, - lang=lang - ) - text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks) - bot.edit_message_text(text, call.from_user.id, call.message.message_id, - reply_markup=keyboards.clicker_keyboard(_, lang)) - - -if __name__ == '__main__': - bot.add_custom_filter(custom_filters.TextMatchFilter()) - bot.infinity_polling() From ab64e174646c7829e9c2934b7c70cfb8110c0408 Mon Sep 17 00:00:00 2001 From: abdullaev388 Date: Sat, 23 Apr 2022 11:48:58 +0500 Subject: [PATCH 2/5] added new i18n class based middleware --- .../i18n_class_based/i18n_base_midddleware.py | 121 ++++++++++ .../class_based/i18n_class_based/keyboards.py | 34 +++ .../locales/en/LC_MESSAGES/messages.po | 81 +++++++ .../locales/ru/LC_MESSAGES/messages.po | 82 +++++++ .../locales/uz_Latn/LC_MESSAGES/messages.po | 80 +++++++ .../class_based/i18n_class_based/main.py | 211 ++++++++++++++++++ 6 files changed, 609 insertions(+) create mode 100644 examples/middleware/class_based/i18n_class_based/i18n_base_midddleware.py create mode 100644 examples/middleware/class_based/i18n_class_based/keyboards.py create mode 100644 examples/middleware/class_based/i18n_class_based/locales/en/LC_MESSAGES/messages.po create mode 100644 examples/middleware/class_based/i18n_class_based/locales/ru/LC_MESSAGES/messages.po create mode 100644 examples/middleware/class_based/i18n_class_based/locales/uz_Latn/LC_MESSAGES/messages.po create mode 100644 examples/middleware/class_based/i18n_class_based/main.py diff --git a/examples/middleware/class_based/i18n_class_based/i18n_base_midddleware.py b/examples/middleware/class_based/i18n_class_based/i18n_base_midddleware.py new file mode 100644 index 0000000..43d7b65 --- /dev/null +++ b/examples/middleware/class_based/i18n_class_based/i18n_base_midddleware.py @@ -0,0 +1,121 @@ +import gettext +import os +import threading + +from telebot.handler_backends import BaseMiddleware + +try: + from babel.support import LazyProxy + + babel_imported = True +except ImportError: + babel_imported = False + + +class I18N(BaseMiddleware): + """ + This middleware provides high-level tool for internationalization + It is based on gettext util. + """ + + context_lang = threading.local() + + def __init__(self, translations_path, domain_name: str): + super().__init__() + self.update_types = self.process_update_types() + + self.path = translations_path + self.domain = domain_name + self.translations = self.find_translations() + + @property + def available_translations(self): + return list(self.translations) + + def gettext(self, text: str, lang: str = None): + """ + Singular translations + """ + + if lang is None: + lang = self.context_lang.language + + if lang not in self.translations: + return text + + translator = self.translations[lang] + return translator.gettext(text) + + def ngettext(self, singular: str, plural: str, lang: str = None, n=1): + """ + Plural translations + """ + if lang is None: + lang = self.context_lang.language + + if lang not in self.translations: + if n == 1: + return singular + return plural + + translator = self.translations[lang] + return translator.ngettext(singular, plural, n) + + def lazy_gettext(self, text: str, lang: str = None): + if not babel_imported: + raise RuntimeError('babel module is not imported. Check that you installed it.') + return LazyProxy(self.gettext, text, lang, enable_cache=False) + + def lazy_ngettext(self, singular: str, plural: str, lang: str = None, n=1): + if not babel_imported: + raise RuntimeError('babel module is not imported. Check that you installed it.') + return LazyProxy(self.ngettext, singular, plural, lang, n, enable_cache=False) + + def get_user_language(self, obj): + """ + You need to override this method and return user language + """ + raise NotImplementedError + + def process_update_types(self) -> list: + """ + You need to override this method and return any update types which you want to be processed + """ + raise NotImplementedError + + def pre_process(self, message, data): + """ + context language variable will be set each time when update from 'process_update_types' comes + value is the result of 'get_user_language' method + """ + + self.context_lang.language = self.get_user_language(obj=message) + + def post_process(self, message, data, exception): + pass + + def find_translations(self): + """ + Looks for translations with passed 'domain' in passed 'path' + """ + if not os.path.exists(self.path): + raise RuntimeError(f"Translations directory by path: {self.path!r} was not found") + + result = {} + + for name in os.listdir(self.path): + translations_path = os.path.join(self.path, name, 'LC_MESSAGES') + + if not os.path.isdir(translations_path): + continue + + po_file = os.path.join(translations_path, self.domain + '.po') + mo_file = po_file[:-2] + 'mo' + + if os.path.isfile(po_file) and not os.path.isfile(mo_file): + raise FileNotFoundError(f"Translations for: {name!r} were not compiled!") + + with open(mo_file, 'rb') as file: + result[name] = gettext.GNUTranslations(file) + + return result diff --git a/examples/middleware/class_based/i18n_class_based/keyboards.py b/examples/middleware/class_based/i18n_class_based/keyboards.py new file mode 100644 index 0000000..14d0473 --- /dev/null +++ b/examples/middleware/class_based/i18n_class_based/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/middleware/class_based/i18n_class_based/locales/en/LC_MESSAGES/messages.po b/examples/middleware/class_based/i18n_class_based/locales/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..2cf8eef --- /dev/null +++ b/examples/middleware/class_based/i18n_class_based/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/middleware/class_based/i18n_class_based/locales/ru/LC_MESSAGES/messages.po b/examples/middleware/class_based/i18n_class_based/locales/ru/LC_MESSAGES/messages.po new file mode 100644 index 0000000..6d330b0 --- /dev/null +++ b/examples/middleware/class_based/i18n_class_based/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/middleware/class_based/i18n_class_based/locales/uz_Latn/LC_MESSAGES/messages.po b/examples/middleware/class_based/i18n_class_based/locales/uz_Latn/LC_MESSAGES/messages.po new file mode 100644 index 0000000..c5bd000 --- /dev/null +++ b/examples/middleware/class_based/i18n_class_based/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/middleware/class_based/i18n_class_based/main.py b/examples/middleware/class_based/i18n_class_based/main.py new file mode 100644 index 0000000..66ae420 --- /dev/null +++ b/examples/middleware/class_based/i18n_class_based/main.py @@ -0,0 +1,211 @@ +""" +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 i18n_base_midddleware import I18N +from telebot import TeleBot +from telebot import types, StateMemoryStorage +from telebot.custom_filters import TextMatchFilter, TextFilter + +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'] + + 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 = TeleBot("", state_storage=storage, use_class_middlewares=True) + +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']) +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) + bot.send_message(message.from_user.id, text) + + +@bot.message_handler(commands=['lang']) +def change_language_handler(message: types.Message): + 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'])) +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 + bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id) + + +@bot.message_handler(commands=['plural']) +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) + + bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_)) + + +@bot.callback_query_handler(func=None, text=TextFilter(equals='click')) +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) + + bot.edit_message_text(text, call.from_user.id, call.message.message_id, + reply_markup=keyboards.clicker_keyboard(_)) + + +@bot.message_handler(commands=['menu']) +def menu_handler(message: types.Message): + text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.") + 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")) +def return_user_id(message: types.Message): + bot.send_message(message.chat.id, str(message.from_user.id)) + + +@bot.message_handler(text=l_("My user name")) +def return_user_id(message: types.Message): + username = message.from_user.username + if not username: + username = '-' + 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)) +def return_user_id(message: types.Message): + 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)) +def missed_message(message: types.Message): + 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()) From b25d2846e935dfab19d611f39f8317bcaaa36cd9 Mon Sep 17 00:00:00 2001 From: abdullaev388 Date: Sat, 23 Apr 2022 11:53:55 +0500 Subject: [PATCH 3/5] TextFilter class supports case insensitiveness with lazy translations --- telebot/asyncio_filters.py | 2 +- telebot/custom_filters.py | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/telebot/asyncio_filters.py b/telebot/asyncio_filters.py index 98874b9..312b290 100644 --- a/telebot/asyncio_filters.py +++ b/telebot/asyncio_filters.py @@ -39,7 +39,7 @@ class TextFilter: """ Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll) - example of usage is in examples/custom_filters/advanced_text_filter.py + example of usage is in examples/asynchronous_telebot/custom_filters/advanced_text_filter.py """ def __init__(self, diff --git a/telebot/custom_filters.py b/telebot/custom_filters.py index 1bd80b3..21a079a 100644 --- a/telebot/custom_filters.py +++ b/telebot/custom_filters.py @@ -69,7 +69,7 @@ class TextFilter: self.ends_with = self._check_iterable(ends_with, filter_name='ends_with') self.ignore_case = ignore_case - def _check_iterable(self, iterable, filter_name: str): + def _check_iterable(self, iterable, filter_name): if not iterable: pass elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple): @@ -95,39 +95,33 @@ class TextFilter: if self.ignore_case: text = text.lower() - - if self.equals: - self.equals = self.equals.lower() - elif self.contains: - self.contains = tuple(map(str.lower, self.contains)) - elif self.starts_with: - self.starts_with = tuple(map(str.lower, self.starts_with)) - elif self.ends_with: - self.ends_with = tuple(map(str.lower, self.ends_with)) + prepare_func = lambda string: str(string).lower() + else: + prepare_func = str if self.equals: - result = self.equals == text + result = prepare_func(self.equals) == text if result: return True elif not result and not any((self.contains, self.starts_with, self.ends_with)): return False if self.contains: - result = any([i in text for i in self.contains]) + result = any([prepare_func(i) in text for i in self.contains]) if result: return True elif not result and not any((self.starts_with, self.ends_with)): return False if self.starts_with: - result = any([text.startswith(i) for i in self.starts_with]) + result = any([text.startswith(prepare_func(i)) for i in self.starts_with]) if result: return True elif not result and not self.ends_with: return False if self.ends_with: - return any([text.endswith(i) for i in self.ends_with]) + return any([text.endswith(prepare_func(i)) for i in self.ends_with]) return False From 3b386965ea212e687d48a50b563bc18db35a48a5 Mon Sep 17 00:00:00 2001 From: abdullaev388 Date: Sun, 24 Apr 2022 11:52:01 +0500 Subject: [PATCH 4/5] sync middleware examples separated into two folders --- .../i18n_base_midddleware.py | 0 .../{i18n_class_based => i18n_middleware}/keyboards.py | 0 .../locales/en/LC_MESSAGES/messages.po | 0 .../locales/ru/LC_MESSAGES/messages.po | 0 .../locales/uz_Latn/LC_MESSAGES/messages.po | 0 .../class_based/{i18n_class_based => i18n_middleware}/main.py | 0 examples/middleware/{ => function_based}/i18n.py | 0 examples/middleware/{ => function_based}/session.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename examples/middleware/class_based/{i18n_class_based => i18n_middleware}/i18n_base_midddleware.py (100%) rename examples/middleware/class_based/{i18n_class_based => i18n_middleware}/keyboards.py (100%) rename examples/middleware/class_based/{i18n_class_based => i18n_middleware}/locales/en/LC_MESSAGES/messages.po (100%) rename examples/middleware/class_based/{i18n_class_based => i18n_middleware}/locales/ru/LC_MESSAGES/messages.po (100%) rename examples/middleware/class_based/{i18n_class_based => i18n_middleware}/locales/uz_Latn/LC_MESSAGES/messages.po (100%) rename examples/middleware/class_based/{i18n_class_based => i18n_middleware}/main.py (100%) rename examples/middleware/{ => function_based}/i18n.py (100%) rename examples/middleware/{ => function_based}/session.py (100%) diff --git a/examples/middleware/class_based/i18n_class_based/i18n_base_midddleware.py b/examples/middleware/class_based/i18n_middleware/i18n_base_midddleware.py similarity index 100% rename from examples/middleware/class_based/i18n_class_based/i18n_base_midddleware.py rename to examples/middleware/class_based/i18n_middleware/i18n_base_midddleware.py diff --git a/examples/middleware/class_based/i18n_class_based/keyboards.py b/examples/middleware/class_based/i18n_middleware/keyboards.py similarity index 100% rename from examples/middleware/class_based/i18n_class_based/keyboards.py rename to examples/middleware/class_based/i18n_middleware/keyboards.py diff --git a/examples/middleware/class_based/i18n_class_based/locales/en/LC_MESSAGES/messages.po b/examples/middleware/class_based/i18n_middleware/locales/en/LC_MESSAGES/messages.po similarity index 100% rename from examples/middleware/class_based/i18n_class_based/locales/en/LC_MESSAGES/messages.po rename to examples/middleware/class_based/i18n_middleware/locales/en/LC_MESSAGES/messages.po diff --git a/examples/middleware/class_based/i18n_class_based/locales/ru/LC_MESSAGES/messages.po b/examples/middleware/class_based/i18n_middleware/locales/ru/LC_MESSAGES/messages.po similarity index 100% rename from examples/middleware/class_based/i18n_class_based/locales/ru/LC_MESSAGES/messages.po rename to examples/middleware/class_based/i18n_middleware/locales/ru/LC_MESSAGES/messages.po diff --git a/examples/middleware/class_based/i18n_class_based/locales/uz_Latn/LC_MESSAGES/messages.po b/examples/middleware/class_based/i18n_middleware/locales/uz_Latn/LC_MESSAGES/messages.po similarity index 100% rename from examples/middleware/class_based/i18n_class_based/locales/uz_Latn/LC_MESSAGES/messages.po rename to examples/middleware/class_based/i18n_middleware/locales/uz_Latn/LC_MESSAGES/messages.po diff --git a/examples/middleware/class_based/i18n_class_based/main.py b/examples/middleware/class_based/i18n_middleware/main.py similarity index 100% rename from examples/middleware/class_based/i18n_class_based/main.py rename to examples/middleware/class_based/i18n_middleware/main.py diff --git a/examples/middleware/i18n.py b/examples/middleware/function_based/i18n.py similarity index 100% rename from examples/middleware/i18n.py rename to examples/middleware/function_based/i18n.py diff --git a/examples/middleware/session.py b/examples/middleware/function_based/session.py similarity index 100% rename from examples/middleware/session.py rename to examples/middleware/function_based/session.py From 24ae38cca64ebb9d97e11b1955c8e29d17f0ae1d Mon Sep 17 00:00:00 2001 From: abdullaev388 Date: Sun, 24 Apr 2022 12:51:01 +0500 Subject: [PATCH 5/5] added function based middleware i18n example --- .../function_based/i18n_gettext/i18n_class.py | 77 ++++++++++ .../function_based/i18n_gettext/keyboards.py | 23 +++ .../locales/en/LC_MESSAGES/messages.po | 50 +++++++ .../locales/ru/LC_MESSAGES/messages.po | 59 ++++++++ .../locales/uz_Latn/LC_MESSAGES/messages.po | 57 ++++++++ .../function_based/i18n_gettext/main.py | 134 ++++++++++++++++++ 6 files changed, 400 insertions(+) create mode 100644 examples/middleware/function_based/i18n_gettext/i18n_class.py create mode 100644 examples/middleware/function_based/i18n_gettext/keyboards.py create mode 100644 examples/middleware/function_based/i18n_gettext/locales/en/LC_MESSAGES/messages.po create mode 100644 examples/middleware/function_based/i18n_gettext/locales/ru/LC_MESSAGES/messages.po create mode 100644 examples/middleware/function_based/i18n_gettext/locales/uz_Latn/LC_MESSAGES/messages.po create mode 100644 examples/middleware/function_based/i18n_gettext/main.py diff --git a/examples/middleware/function_based/i18n_gettext/i18n_class.py b/examples/middleware/function_based/i18n_gettext/i18n_class.py new file mode 100644 index 0000000..68b3ffd --- /dev/null +++ b/examples/middleware/function_based/i18n_gettext/i18n_class.py @@ -0,0 +1,77 @@ +import gettext +import os +import threading + + +class I18N: + """ + This class provides high-level tool for internationalization + It is based on gettext util. + """ + + context_lang = threading.local() + + def __init__(self, translations_path, domain_name: str): + self.path = translations_path + self.domain = domain_name + self.translations = self.find_translations() + + @property + def available_translations(self): + return list(self.translations) + + def gettext(self, text: str, lang: str = None): + """ + Singular translations + """ + + if lang is None: + lang = self.context_lang.language + + if lang not in self.translations: + return text + + translator = self.translations[lang] + return translator.gettext(text) + + def ngettext(self, singular: str, plural: str, lang: str = None, n=1): + """ + Plural translations + """ + if lang is None: + lang = self.context_lang.language + + if lang not in self.translations: + if n == 1: + return singular + return plural + + translator = self.translations[lang] + return translator.ngettext(singular, plural, n) + + + def find_translations(self): + """ + Looks for translations with passed 'domain' in passed 'path' + """ + if not os.path.exists(self.path): + raise RuntimeError(f"Translations directory by path: {self.path!r} was not found") + + result = {} + + for name in os.listdir(self.path): + translations_path = os.path.join(self.path, name, 'LC_MESSAGES') + + if not os.path.isdir(translations_path): + continue + + po_file = os.path.join(translations_path, self.domain + '.po') + mo_file = po_file[:-2] + 'mo' + + if os.path.isfile(po_file) and not os.path.isfile(mo_file): + raise FileNotFoundError(f"Translations for: {name!r} were not compiled!") + + with open(mo_file, 'rb') as file: + result[name] = gettext.GNUTranslations(file) + + return result diff --git a/examples/middleware/function_based/i18n_gettext/keyboards.py b/examples/middleware/function_based/i18n_gettext/keyboards.py new file mode 100644 index 0000000..cdda2ae --- /dev/null +++ b/examples/middleware/function_based/i18n_gettext/keyboards.py @@ -0,0 +1,23 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + + +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'), + ] + ] + ) diff --git a/examples/middleware/function_based/i18n_gettext/locales/en/LC_MESSAGES/messages.po b/examples/middleware/function_based/i18n_gettext/locales/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..5931c28 --- /dev/null +++ b/examples/middleware/function_based/i18n_gettext/locales/en/LC_MESSAGES/messages.po @@ -0,0 +1,50 @@ +# 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-18 17:54+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 "" + +#: main.py:78 +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 "" + +#: main.py:102 +msgid "Language has been changed" +msgstr "" + +#: main.py:114 +#, fuzzy +msgid "You have {number} click" +msgid_plural "You have {number} clicks" +msgstr[0] "" +msgstr[1] "" + +#: main.py:120 +msgid "" +"This is clicker.\n" +"\n" +msgstr "" \ No newline at end of file diff --git a/examples/middleware/function_based/i18n_gettext/locales/ru/LC_MESSAGES/messages.po b/examples/middleware/function_based/i18n_gettext/locales/ru/LC_MESSAGES/messages.po new file mode 100644 index 0000000..79db9b5 --- /dev/null +++ b/examples/middleware/function_based/i18n_gettext/locales/ru/LC_MESSAGES/messages.po @@ -0,0 +1,59 @@ +# 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-18 17:54+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 "Клик" + +#: main.py:78 +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 "" +"Привет, {user_fist_name}!\n" +"Это пример мультиязычного бота.\n" +"Доступные команды:\n" +"\n" +"/lang - изменить язык\n" +"/plural - пример плюрализации" + +#: main.py:102 +msgid "Language has been changed" +msgstr "Язык был сменён" + +#: main.py:114 +msgid "You have {number} click" +msgid_plural "You have {number} clicks" +msgstr[0] "У вас {number} клик" +msgstr[1] "У вас {number} клика" +msgstr[2] "У вас {number} кликов" + +#: main.py:120 +msgid "" +"This is clicker.\n" +"\n" +msgstr "" +"Это кликер.\n" +"\n" \ No newline at end of file diff --git a/examples/middleware/function_based/i18n_gettext/locales/uz_Latn/LC_MESSAGES/messages.po b/examples/middleware/function_based/i18n_gettext/locales/uz_Latn/LC_MESSAGES/messages.po new file mode 100644 index 0000000..ce7b502 --- /dev/null +++ b/examples/middleware/function_based/i18n_gettext/locales/uz_Latn/LC_MESSAGES/messages.po @@ -0,0 +1,57 @@ +# 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-18 17:54+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" + +#: main.py:78 +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 "" +"Salom, {user_fist_name}!\n" +"Bu multilanguage bot misoli.\n" +"Mavjud buyruqlar:\n" +"\n" +"/lang - tilni ozgartirish\n" +"/plural - pluralizatsiya misoli" + +#: main.py:102 +msgid "Language has been changed" +msgstr "Til ozgartirildi" + +#: main.py:114 +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:120 +msgid "" +"This is clicker.\n" +"\n" +msgstr "" +"Bu clicker.\n" +"\n" diff --git a/examples/middleware/function_based/i18n_gettext/main.py b/examples/middleware/function_based/i18n_gettext/main.py new file mode 100644 index 0000000..c40b00c --- /dev/null +++ b/examples/middleware/function_based/i18n_gettext/main.py @@ -0,0 +1,134 @@ +""" +In this example you will learn how to adapt your bot to different languages +Using built-in class 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 +""" + +from telebot import TeleBot, types, custom_filters +from telebot import apihelper +from telebot.storage.memory_storage import StateMemoryStorage + +import keyboards +from i18n_class import I18N + +apihelper.ENABLE_MIDDLEWARE = True +storage = StateMemoryStorage() +# IMPORTANT! This example works only if polling is non-threaded. +bot = TeleBot("", state_storage=storage, threaded=False) + +i18n = I18N(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.middleware_handler(update_types=['message', 'callback_query']) +def set_contex_language(bot_instance, message): + i18n.context_lang.language = users_lang.get(message.from_user.id, 'en') + + +@bot.message_handler(commands=['start']) +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") + + # remember don't use f string for interpolation, use .format method instead + text = text.format(user_fist_name=message.from_user.first_name) + bot.send_message(message.from_user.id, text) + + +@bot.message_handler(commands=['lang']) +def change_language_handler(message: types.Message): + bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang", + reply_markup=keyboards.languages_keyboard()) + + +@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(contains=['en', 'ru', 'uz_Latn'])) +def language_handler(call: types.CallbackQuery): + lang = call.data + users_lang[call.from_user.id] = lang + + # When you change user's language, pass language explicitly coz it's not changed in context + bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id) + bot.delete_state(call.from_user.id) + + +@bot.message_handler(commands=['plural']) +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) + bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_)) + + +@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click')) +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) + bot.edit_message_text(text, call.from_user.id, call.message.message_id, + reply_markup=keyboards.clicker_keyboard(_)) + + +if __name__ == '__main__': + bot.add_custom_filter(custom_filters.TextMatchFilter()) + bot.infinity_polling()