From 26d531a304a4ccb73d83a428e2a106ef953d0e44 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 21 Sep 2021 03:10:16 +0300 Subject: [PATCH] Added docs --- aiogram/utils/i18n/middleware.py | 76 +++++++++++- docs/_static/stylesheets/extra.css | 10 -- docs/index.rst | 1 + docs/utils/i18n.rst | 190 +++++++++++++++++++++++++++++ docs/utils/index.rst | 7 ++ 5 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 docs/utils/i18n.rst create mode 100644 docs/utils/index.rst diff --git a/aiogram/utils/i18n/middleware.py b/aiogram/utils/i18n/middleware.py index bcf11a50..3c810256 100644 --- a/aiogram/utils/i18n/middleware.py +++ b/aiogram/utils/i18n/middleware.py @@ -14,12 +14,23 @@ from aiogram.utils.i18n.core import I18n class I18nMiddleware(BaseMiddleware, ABC): + """ + Abstract I18n middleware. + """ + def __init__( self, i18n: I18n, i18n_key: Optional[str] = "i18n", middleware_key: str = "i18n_middleware", ) -> None: + """ + Create an instance of middleware + + :param i18n: instance of I18n + :param i18n_key: context key for I18n instance + :param middleware_key: context key for this middleware + """ self.i18n = i18n self.i18n_key = i18n_key self.middleware_key = middleware_key @@ -27,6 +38,13 @@ class I18nMiddleware(BaseMiddleware, ABC): def setup( self: BaseMiddleware, router: Router, exclude: Optional[Set[str]] = None ) -> BaseMiddleware: + """ + Register middleware for all events in the Router + + :param router: + :param exclude: + :return: + """ if exclude is None: exclude = set() exclude_events = {"update", "error", *exclude} @@ -56,12 +74,32 @@ class I18nMiddleware(BaseMiddleware, ABC): @abstractmethod async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str: + """ + Detect current user locale based on event and context. + + **This method must be defined in child classes** + + :param event: + :param data: + :return: + """ pass class SimpleI18nMiddleware(I18nMiddleware): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """ + Simple I18n middleware. + + Chooses language code from the User object received in event + """ + + def __init__( + self, + i18n: I18n, + i18n_key: Optional[str] = "i18n", + middleware_key: str = "i18n_middleware", + ) -> None: + super().__init__(i18n=i18n, i18n_key=i18n_key, middleware_key=middleware_key) if Locale is None: # pragma: no cover raise RuntimeError( @@ -88,8 +126,18 @@ class SimpleI18nMiddleware(I18nMiddleware): class ConstI18nMiddleware(I18nMiddleware): - def __init__(self, locale: str, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """ + Const middleware chooses statically defined locale + """ + + def __init__( + self, + locale: str, + i18n: I18n, + i18n_key: Optional[str] = "i18n", + middleware_key: str = "i18n_middleware", + ) -> None: + super().__init__(i18n=i18n, i18n_key=i18n_key, middleware_key=middleware_key) self.locale = locale async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str: @@ -97,8 +145,18 @@ class ConstI18nMiddleware(I18nMiddleware): class FSMI18nMiddleware(SimpleI18nMiddleware): - def __init__(self, *args: Any, key: str = "locale", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """ + This middleware stores locale in the FSM storage + """ + + def __init__( + self, + i18n: I18n, + key: str = "locale", + i18n_key: Optional[str] = "i18n", + middleware_key: str = "i18n_middleware", + ) -> None: + super().__init__(i18n=i18n, i18n_key=i18n_key, middleware_key=middleware_key) self.key = key async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str: @@ -114,5 +172,11 @@ class FSMI18nMiddleware(SimpleI18nMiddleware): return locale async def set_locale(self, state: FSMContext, locale: str) -> None: + """ + Write new locale to the storage + + :param state: instance of FSMContext + :param locale: new locale + """ await state.update_data(data={self.key: locale}) self.i18n.current_locale = locale diff --git a/docs/_static/stylesheets/extra.css b/docs/_static/stylesheets/extra.css index 5b653b0c..b35d291c 100644 --- a/docs/_static/stylesheets/extra.css +++ b/docs/_static/stylesheets/extra.css @@ -10,13 +10,3 @@ code, kbd, pre { font-family: "JetBrainsMono", "Roboto Mono", "Courier New", Courier, monospace; } - -.highlight * { - background: #f0f0f0; -} - -@media (prefers-color-scheme: dark) { - .highlight * { - background: #424242; - } -} diff --git a/docs/index.rst b/docs/index.rst index 0016a02e..ffd29dbb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -93,4 +93,5 @@ Contents install api/index dispatcher/index + utils/index changelog diff --git a/docs/utils/i18n.rst b/docs/utils/i18n.rst new file mode 100644 index 00000000..2c36c85d --- /dev/null +++ b/docs/utils/i18n.rst @@ -0,0 +1,190 @@ +=========== +Translation +=========== + +In order to make you bot translatable you have to add a minimal number of hooks to your Python code. + +These hooks are called translation strings. + +The aiogram translation utils is build on top of `GNU gettext Python module `_ +and `Babel library `_. + +Installation +============ + +Babel is required to make simple way to extract translation strings from your code + +Can be installed from pip directly: + +.. code-block:: bash + + pip install Babel + + +or as `aiogram` extra dependency: + +.. code-block:: bash + + pip install aiogram[i18n] + + +Make messages translatable +========================== + +In order to gettext need to know what the strings should be translated you will need to write translation strings. + +For example: + +.. code-block:: python + :emphasize-lines: 6-8 + + from aiogram import html + from aiogram.utils.i18n import gettext as _ + + async def my_handler(message: Message) -> None: + await message.answer( + _("Hello, {name}!").format( + name=html.quote(message.from_user.full_name) + ) + ) + + +.. danger:: + + f-strings can't be used as translations string because any dynamic variables should be added to message after getting translated message + + +Also if you want to use translated string in keyword- or magic- filters you will need to use lazy gettext calls: + + +.. code-block:: python + :emphasize-lines: 4 + + from aiogram import F + from aiogram.utils.i18n import lazy_gettext as __ + + @router.message(F.text.lower() == __("My menu entry")) + ... + + +.. danger:: + + Lazy gettext calls should always be used when the current language is not know at the moment + + +.. danger:: + + Lazy gettext can't be used as value for API methods or any Telegram Object (like :class:`aiogram.types.inline_keyboard_button.InlineKeyboardButton` or etc.) + +Configuring engine +================== + +After you messages is already done to use gettext your bot should know how to detect user language + +On top of your application the instance of :class:`aiogram.utils.i18n.code.I18n` should be created + + +.. code-block:: + + i18n = I18n(path="locales", language="en", domain="messages") + + +After that you will need to choose one of builtin I18n middleware or write your own. + +Builtin middlewares: + + +SimpleI18nMiddleware +~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: aiogram.utils.i18n.middleware.SimpleI18nMiddleware + :members: __init__ + +ConstI18nMiddleware +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: aiogram.utils.i18n.middleware.ConstI18nMiddleware + :members: __init__ + +FSMI18nMiddleware +~~~~~~~~~~~~~~~~~ + +.. autoclass:: aiogram.utils.i18n.middleware.FSMI18nMiddleware + :members: __init__,set_locale + + +I18nMiddleware +~~~~~~~~~~~~~~ + +or define you own based on abstract I18nMiddleware middleware: + +.. autoclass:: aiogram.utils.i18n.middleware.FSMI18nMiddleware + :members: __init__,setup,get_locale + + +Deal with Babel +=============== + +Step 1: Extract messages +------------------- + +.. code-block:: bash + + pybabel extract --input-dirs=. -o locales/messages.pot + + +Here is :code:`--input-dirs=.` - path to code and the :code:`locales/messages.pot` +is template where messages will be extracted and `messages` is translation domain. + +.. note:: + + Some useful options: + + - Extract texts with pluralization support :code:`-k __:1,2` + - Add comments for translators, you can use another tag if you want (TR) :code:`--add-comments=NOTE` + - Disable comments with string location in code :code:`--no-location` + - Set project name :code:`--project=MySuperBot` + - Set version :code:`--version=2.2` + + +Step 2: Init language +---------------- + +.. code-block:: bash + + pybabel init -i locales/messages.pot -d locales -D messages -l en + +- :code:`-i locales/messages.pot` - pre-generated template +- :code:`-d locales` - translations directory +- :code:`-D messages` - translations domain +- :code:`-l en` - language. Can be changed to any other valid language code (For example :code:`-l uk` for ukrainian language) + + +Step 3: Translate texts +----------------------- + +To open .po file you can use basic text editor or any PO editor, e.g. `Poedit `_ + +Just open the file named :code:`locales/{language}/LC_MESSAGES/messages.po` and write translations + +Step 4: Compile translations +---------------------------- + +.. code-block:: bash + + pybabel compile -d locales -D messages + + +Step 5: Updating messages +------------------------- + +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 + .. code-block:: + + pybabel update -d locales -D messages -i locales/messages.pot + +- Step 5.3: update your translations: location and tools you know from step 3 +- Step 5.4: compile mo files: command from step 4 diff --git a/docs/utils/index.rst b/docs/utils/index.rst new file mode 100644 index 00000000..a362e9f8 --- /dev/null +++ b/docs/utils/index.rst @@ -0,0 +1,7 @@ +===== +Utils +===== + +.. toctree:: + + i18n