diff --git a/.gitignore b/.gitignore
index 6c2a9404..a8b34bd1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,3 +57,6 @@ experiment.py
# Doc's
docs/html
+
+# i18n/l10n
+*.mo
diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py
new file mode 100644
index 00000000..3b60e070
--- /dev/null
+++ b/aiogram/contrib/middlewares/i18n.py
@@ -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
diff --git a/examples/i18n_example.py b/examples/i18n_example.py
new file mode 100644
index 00000000..a7ccbde3
--- /dev/null
+++ b/examples/i18n_example.py
@@ -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, {user}!').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: {language}').format(language=locale))
+
+
+if __name__ == '__main__':
+ executor.start_polling(dp, skip_updates=True)
diff --git a/examples/locales/en/LC_MESSAGES/mybot.po b/examples/locales/en/LC_MESSAGES/mybot.po
new file mode 100644
index 00000000..75970929
--- /dev/null
+++ b/examples/locales/en/LC_MESSAGES/mybot.po
@@ -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 , 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 \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.6.0\n"
+
+#: i18n_example.py:48
+msgid "Hello, {user}!"
+msgstr ""
+
+#: i18n_example.py:53
+msgid "Your current language: {language}"
+msgstr ""
+
diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot
new file mode 100644
index 00000000..988ed463
--- /dev/null
+++ b/examples/locales/mybot.pot
@@ -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 , 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 \n"
+"Language-Team: LANGUAGE \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, {user}!"
+msgstr ""
+
+#: i18n_example.py:53
+msgid "Your current language: {language}"
+msgstr ""
+
diff --git a/examples/locales/ru/LC_MESSAGES/mybot.po b/examples/locales/ru/LC_MESSAGES/mybot.po
new file mode 100644
index 00000000..73876f30
--- /dev/null
+++ b/examples/locales/ru/LC_MESSAGES/mybot.po
@@ -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 , 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 \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.6.0\n"
+
+#: i18n_example.py:48
+msgid "Hello, {user}!"
+msgstr "Привет, {user}!"
+
+#: i18n_example.py:53
+msgid "Your current language: {language}"
+msgstr "Твой язык: {language}"
+
diff --git a/examples/locales/uk/LC_MESSAGES/mybot.po b/examples/locales/uk/LC_MESSAGES/mybot.po
new file mode 100644
index 00000000..25970c19
--- /dev/null
+++ b/examples/locales/uk/LC_MESSAGES/mybot.po
@@ -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 , 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 \n"
+"Language: uk\n"
+"Language-Team: uk \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, {user}!"
+msgstr "Привіт, {user}!"
+
+#: i18n_example.py:53
+msgid "Your current language: {language}"
+msgstr "Твоя мова: {language}"
+