From 53ad5377392c84c49bd8254e884cd3ddcae138b3 Mon Sep 17 00:00:00 2001 From: mahisafa82 Date: Thu, 26 Feb 2026 03:07:37 +0330 Subject: [PATCH] i18n: normalize Telegram region codes in locale detection Resolve user locale by checking the Telegram language code as-is, then its Babel-normalized form, then the base language. This fixes lowercase regional codes such as pt-br failing to match available translations like pt_BR. Add tests covering region-code variants and fallback behavior, and update i18n documentation plus changelog notes. --- CHANGES/1755.bugfix.rst | 1 + aiogram/utils/i18n/middleware.py | 12 +++++++---- docs/utils/i18n.rst | 10 +++++++++ tests/test_utils/test_i18n.py | 36 ++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 CHANGES/1755.bugfix.rst diff --git a/CHANGES/1755.bugfix.rst b/CHANGES/1755.bugfix.rst new file mode 100644 index 00000000..5a500a54 --- /dev/null +++ b/CHANGES/1755.bugfix.rst @@ -0,0 +1 @@ +Improved i18n locale detection in ``SimpleI18nMiddleware`` to support Telegram-style region codes such as ``pt-br`` by resolving first the raw code and then the normalized Babel-style code (for example ``pt_BR``). \ No newline at end of file diff --git a/aiogram/utils/i18n/middleware.py b/aiogram/utils/i18n/middleware.py index 462f4db0..c893a336 100644 --- a/aiogram/utils/i18n/middleware.py +++ b/aiogram/utils/i18n/middleware.py @@ -129,14 +129,18 @@ class SimpleI18nMiddleware(I18nMiddleware): event_from_user: User | None = data.get("event_from_user") if event_from_user is None or event_from_user.language_code is None: return self.i18n.default_locale + + user_locale = event_from_user.language_code try: - locale = Locale.parse(event_from_user.language_code, sep="-") + locale = Locale.parse(user_locale, sep="-") except UnknownLocaleError: return self.i18n.default_locale - if locale.language not in self.i18n.available_locales: - return self.i18n.default_locale - return locale.language + for candidate in (user_locale, str(locale), locale.language): + if candidate in self.i18n.available_locales: + return candidate + + return self.i18n.default_locale class ConstI18nMiddleware(I18nMiddleware): diff --git a/docs/utils/i18n.rst b/docs/utils/i18n.rst index 2ea2c2dc..cd56817b 100644 --- a/docs/utils/i18n.rst +++ b/docs/utils/i18n.rst @@ -113,6 +113,16 @@ On top of your application the instance of :class:`aiogram.utils.i18n.I18n` shou After that you will need to choose one of builtin I18n middleware or write your own. +When using :class:`aiogram.utils.i18n.middleware.SimpleI18nMiddleware`, locale codes from +Telegram (for example ``pt-br``) are resolved against loaded locales in two steps: + +1. Direct match as received from Telegram (``pt-br``) +2. Normalized Babel-style identifier (``pt_BR``) + +So if your translations are stored under +``locales/pt_BR/LC_MESSAGES/messages.mo``, they will be selected for users with +``language_code="pt-br"``. + Builtin middlewares: diff --git a/tests/test_utils/test_i18n.py b/tests/test_utils/test_i18n.py index d6dc2444..2fb795da 100644 --- a/tests/test_utils/test_i18n.py +++ b/tests/test_utils/test_i18n.py @@ -149,6 +149,42 @@ class TestSimpleI18nMiddleware: ) assert locale == i18n.default_locale + @pytest.mark.parametrize( + ("available_locales", "default_locale", "language_code", "expected_locale"), + [ + ({"pt-br": None}, "en", "pt-br", "pt-br"), + ({"pt_BR": None}, "en", "pt-br", "pt_BR"), + ({"pt-br": None, "pt_BR": None}, "en", "pt-br", "pt-br"), + ({"en": None}, "uk", "en-US", "en"), + ({"uk": None}, "en", "uk-UA", "uk"), + ], + ) + async def test_get_locale_region_code_variants( + self, + i18n: I18n, + available_locales: dict[str, None], + default_locale: str, + language_code: str, + expected_locale: str, + ): + i18n.default_locale = default_locale + middleware = SimpleI18nMiddleware(i18n=i18n) + i18n.locales = available_locales + + locale = await middleware.get_locale( + None, + { + "event_from_user": User( + id=42, + is_bot=False, + first_name="Test", + language_code=language_code, + ) + }, + ) + + assert locale == expected_locale + async def test_custom_keys(self, i18n: I18n): async def handler(event, data): return data