Merge pull request #1456 from abdullaev388/master

I18N class for sync telebot and middleware for async
This commit is contained in:
Badiboy 2022-02-22 22:37:13 +03:00 committed by GitHub
commit 292df419ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1042 additions and 21 deletions

View File

@ -0,0 +1,120 @@
import contextvars
import gettext
import os
from telebot.asyncio_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 = contextvars.ContextVar('language', default=None)
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.get()
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.get()
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)
async 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
async 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.set(await self.get_user_language(obj=message))
async 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

View File

@ -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

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\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 ""

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\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 "Кажется, вы перепутали язык"

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: uz_Latn\n"
"Language-Team: uz_Latn <LL@li.org>\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"

View File

@ -0,0 +1,214 @@
"""
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())

View File

@ -0,0 +1,66 @@
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

View File

@ -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(_, lang):
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'),
]
]
)

View File

@ -0,0 +1,51 @@
# English translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\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 ""

View File

@ -0,0 +1,60 @@
# Russian translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\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"

View File

@ -0,0 +1,58 @@
# Uzbek (Latin) translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: uz_Latn\n"
"Language-Team: uz_Latn <LL@li.org>\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"

View File

@ -0,0 +1,153 @@
"""
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()

View File

@ -92,39 +92,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

View File

@ -1,14 +1,15 @@
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
"""
def __init__(self):
pass
async def pre_process(self, message, data):
raise NotImplementedError
async def post_process(self, message, data, exception):
raise NotImplementedError
@ -16,9 +17,10 @@ class BaseMiddleware:
class State:
def __init__(self) -> None:
self.name = None
def __str__(self) -> str:
return self.name
class StatesGroup:
def __init_subclass__(cls) -> None:
@ -26,4 +28,4 @@ class StatesGroup:
for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
value.name = ':'.join((cls.__name__, name))

View File

@ -21,6 +21,7 @@ try:
# noinspection PyPackageRequirements
from PIL import Image
from io import BytesIO
pil_imported = True
except:
pil_imported = False
@ -39,16 +40,17 @@ content_type_media = [
content_type_service = [
'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
]
update_types = [
"update_id", "message", "edited_message", "channel_post", "edited_channel_post", "inline_query",
"chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer",
"update_id", "message", "edited_message", "channel_post", "edited_channel_post", "inline_query",
"chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer",
"my_chat_member", "chat_member", "chat_join_request"
]
class WorkerThread(threading.Thread):
count = 0
@ -217,7 +219,7 @@ def pil_image_to_file(image, extension='JPEG', quality='web_low'):
photoBuffer = BytesIO()
image.convert('RGB').save(photoBuffer, extension, quality=quality)
photoBuffer.seek(0)
return photoBuffer
else:
raise RuntimeError('PIL module is not imported')
@ -338,7 +340,7 @@ def user_link(user: types.User, include_id: bool=False) -> str:
:return: HTML user link
"""
name = escape(user.first_name)
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
+ (f" (<pre>{user.id}</pre>)" if include_id else ""))
@ -477,6 +479,7 @@ def webhook_google_functions(bot, request):
else:
return 'Bot ON'
def antiflood(function, *args, **kwargs):
"""
Use this function inside loops in order to avoid getting TooManyRequests error.