mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-06 07:50:32 +00:00
Implement i18n middleware and add example.
This commit is contained in:
parent
69c126e027
commit
cdc51a6994
7 changed files with 310 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -57,3 +57,6 @@ experiment.py
|
|||
|
||||
# Doc's
|
||||
docs/html
|
||||
|
||||
# i18n/l10n
|
||||
*.mo
|
||||
|
|
|
|||
137
aiogram/contrib/middlewares/i18n.py
Normal file
137
aiogram/contrib/middlewares/i18n.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import gettext
|
||||
import os
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from babel import Locale
|
||||
|
||||
from ... import types
|
||||
from ...dispatcher.middlewares import BaseMiddleware
|
||||
|
||||
|
||||
class I18nMiddleware(BaseMiddleware):
|
||||
"""
|
||||
I18n middleware based on gettext util
|
||||
|
||||
>>> dp = Dispatcher(bot)
|
||||
>>> i18n = I18nMiddleware(DOMAIN, LOCALES_DIR)
|
||||
>>> dp.middleware.setup(i18n)
|
||||
and then
|
||||
>>> _ = i18n.gettext
|
||||
or
|
||||
>>> _ = i18n = I18nMiddleware(DOMAIN_NAME, LOCALES_DIR)
|
||||
"""
|
||||
|
||||
ctx_locale = ContextVar('ctx_user_locale', default=None)
|
||||
|
||||
def __init__(self, domain, path=None, default='en'):
|
||||
"""
|
||||
:param domain: domain
|
||||
:param path: path where located all *.mo files
|
||||
:param default: default locale name
|
||||
"""
|
||||
super(I18nMiddleware, self).__init__()
|
||||
|
||||
if path is None:
|
||||
path = os.path.join(os.getcwd(), 'locales')
|
||||
|
||||
self.domain = domain
|
||||
self.path = path
|
||||
self.default = default
|
||||
|
||||
self.locales = self.find_locales()
|
||||
|
||||
def find_locales(self) -> Dict[str, gettext.GNUTranslations]:
|
||||
"""
|
||||
Load all compiled locales from path
|
||||
|
||||
:return: dict with locales
|
||||
"""
|
||||
translations = {}
|
||||
|
||||
for name in os.listdir(self.path):
|
||||
if not os.path.isdir(self.path):
|
||||
continue
|
||||
mo_path = os.path.join(self.path, name, 'LC_MESSAGES', self.domain + '.mo')
|
||||
|
||||
if os.path.exists(mo_path):
|
||||
with open(mo_path, 'rb') as fp:
|
||||
translations[name] = gettext.GNUTranslations(fp)
|
||||
|
||||
return translations
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
Hot reload locles
|
||||
"""
|
||||
self.locales = self.find_locales()
|
||||
|
||||
@property
|
||||
def available_locales(self) -> Tuple[str]:
|
||||
"""
|
||||
list of loaded locales
|
||||
|
||||
:return:
|
||||
"""
|
||||
return tuple(self.locales.keys())
|
||||
|
||||
def __call__(self, singular, plural=None, n=1, locale=None) -> str:
|
||||
return self.gettext(singular, plural, n, locale)
|
||||
|
||||
def gettext(self, singular, plural=None, n=1, locale=None) -> str:
|
||||
"""
|
||||
Get text
|
||||
|
||||
:param singular:
|
||||
:param plural:
|
||||
:param n:
|
||||
:param locale:
|
||||
:return:
|
||||
"""
|
||||
if locale is None:
|
||||
locale = self.ctx_locale.get()
|
||||
|
||||
if locale not in self.locales:
|
||||
if n is 1:
|
||||
return singular
|
||||
else:
|
||||
return plural
|
||||
|
||||
translator = self.locales[locale]
|
||||
|
||||
if plural is None:
|
||||
return translator.gettext(singular)
|
||||
else:
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
|
||||
"""
|
||||
User locale getter
|
||||
You can override the method if you want to use different way of getting user language.
|
||||
|
||||
:param action: event name
|
||||
:param args: event arguments
|
||||
:return: locale name
|
||||
"""
|
||||
user: types.User = types.User.current()
|
||||
locale: Locale = user.locale
|
||||
|
||||
if locale:
|
||||
*_, data = args
|
||||
language = data['locale'] = locale.language
|
||||
return language
|
||||
|
||||
async def trigger(self, action, args):
|
||||
"""
|
||||
Event trigger
|
||||
|
||||
:param action: event name
|
||||
:param args: event arguments
|
||||
:return:
|
||||
"""
|
||||
if 'update' not in action \
|
||||
and 'error' not in action \
|
||||
and action.startswith('pre_process'):
|
||||
locale = await self.get_user_locale(action, args)
|
||||
self.ctx_locale.set(locale)
|
||||
return True
|
||||
57
examples/i18n_example.py
Normal file
57
examples/i18n_example.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Internalize your bot
|
||||
|
||||
Step 1: extract texts
|
||||
# pybabel extract i18n_example.py -o locales/mybot.pot
|
||||
Step 2: create *.po files. For e.g. create en, ru, uk locales.
|
||||
# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l
|
||||
Step 3: translate texts
|
||||
Step 4: compile translations
|
||||
# pybabel compile -d locales -D mybot
|
||||
|
||||
Step 5: When you change the code of your bot you need to update po & mo files.
|
||||
Step 5.1: regenerate pot file:
|
||||
command from step 1
|
||||
Step 5.2: update po files
|
||||
# pybabel update -d locales -D mybot -i locales/mybot.pot
|
||||
Step 5.3: update your translations
|
||||
Step 5.4: compile mo files
|
||||
command from step 4
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from aiogram import Bot, Dispatcher, types
|
||||
from aiogram.contrib.middlewares.i18n import I18nMiddleware
|
||||
from aiogram.utils import executor
|
||||
|
||||
TOKEN = 'BOT TOKEN HERE'
|
||||
I18N_DOMAIN = 'mybot'
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
LOCALES_DIR = BASE_DIR / 'locales'
|
||||
|
||||
bot = Bot(TOKEN, parse_mode=types.ParseMode.HTML)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
# Setup i18n middleware
|
||||
i18n = I18nMiddleware(I18N_DOMAIN, LOCALES_DIR)
|
||||
dp.middleware.setup(i18n)
|
||||
|
||||
# Alias for gettext method
|
||||
_ = i18n.gettext
|
||||
|
||||
|
||||
@dp.message_handler(commands=['start'])
|
||||
async def cmd_start(message: types.Message):
|
||||
# Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts.
|
||||
await message.reply(_('Hello, <b>{user}</b>!').format(user=message.from_user.full_name))
|
||||
|
||||
|
||||
@dp.message_handler(commands=['lang'])
|
||||
async def cmd_lang(message: types.Message, locale):
|
||||
await message.reply(_('Your current language: <i>{language}</i>').format(language=locale))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
28
examples/locales/en/LC_MESSAGES/mybot.po
Normal file
28
examples/locales/en/LC_MESSAGES/mybot.po
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# English translations for PROJECT.
|
||||
# Copyright (C) 2018 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2018-06-30 03:50+0300\n"
|
||||
"PO-Revision-Date: 2018-06-30 03:43+0300\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.6.0\n"
|
||||
|
||||
#: i18n_example.py:48
|
||||
msgid "Hello, <b>{user}</b>!"
|
||||
msgstr ""
|
||||
|
||||
#: i18n_example.py:53
|
||||
msgid "Your current language: <i>{language}</i>"
|
||||
msgstr ""
|
||||
|
||||
27
examples/locales/mybot.pot
Normal file
27
examples/locales/mybot.pot
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Translations template for PROJECT.
|
||||
# Copyright (C) 2018 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2018-06-30 03:50+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.6.0\n"
|
||||
|
||||
#: i18n_example.py:48
|
||||
msgid "Hello, <b>{user}</b>!"
|
||||
msgstr ""
|
||||
|
||||
#: i18n_example.py:53
|
||||
msgid "Your current language: <i>{language}</i>"
|
||||
msgstr ""
|
||||
|
||||
29
examples/locales/ru/LC_MESSAGES/mybot.po
Normal file
29
examples/locales/ru/LC_MESSAGES/mybot.po
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Russian translations for PROJECT.
|
||||
# Copyright (C) 2018 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2018-06-30 03:50+0300\n"
|
||||
"PO-Revision-Date: 2018-06-30 03:43+0300\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.6.0\n"
|
||||
|
||||
#: i18n_example.py:48
|
||||
msgid "Hello, <b>{user}</b>!"
|
||||
msgstr "Привет, <b>{user}</b>!"
|
||||
|
||||
#: i18n_example.py:53
|
||||
msgid "Your current language: <i>{language}</i>"
|
||||
msgstr "Твой язык: <i>{language}</i>"
|
||||
|
||||
29
examples/locales/uk/LC_MESSAGES/mybot.po
Normal file
29
examples/locales/uk/LC_MESSAGES/mybot.po
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Ukrainian translations for PROJECT.
|
||||
# Copyright (C) 2018 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2018-06-30 03:50+0300\n"
|
||||
"PO-Revision-Date: 2018-06-30 03:43+0300\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: uk\n"
|
||||
"Language-Team: uk <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.6.0\n"
|
||||
|
||||
#: i18n_example.py:48
|
||||
msgid "Hello, <b>{user}</b>!"
|
||||
msgstr "Привіт, <b>{user}</b>!"
|
||||
|
||||
#: i18n_example.py:53
|
||||
msgid "Your current language: <i>{language}</i>"
|
||||
msgstr "Твоя мова: <i>{language}</i>"
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue