aiogram/aiogram/utils/i18n/core.py
andrew000 e94e33c496
Drop py3.9 and pypy3.9
Add pypy3.11 (testing) into `tests.yml`

Remove py3.9 from matrix in `tests.yml`

Refactor not auto-gen code to be compatible with py3.10+, droping ugly 3.9 annotation.

Replace some `from typing` imports to `from collections.abc`, due to deprecation

Add `from __future__ import annotations` and `if TYPE_CHECKING:` where possible

Add some `noqa` to calm down Ruff in some places, if Ruff will be used as default linting+formatting tool in future

Replace some relative imports to absolute

Sort `__all__` tuples in `__init__.py` and some other `.py` files

Sort `__slots__` tuples in classes

Split raises into `msg` and `raise` (`EM101`, `EM102`) to not duplicate error message in the traceback

Add `Self` from `typing_extenstion` where possible

Resolve typing problem in `aiogram/filters/command.py:18`

Concatenate nested `if` statements

Convert `HandlerContainer` into a dataclass in `aiogram/fsm/scene.py`

Bump tests docker-compose.yml `redis:6-alpine` -> `redis:8-alpine`

Bump tests docker-compose.yml `mongo:7.0.6` -> `mongo:8.0.14`

Bump pre-commit-config `black==24.4.2` -> `black==25.9.0`

Bump pre-commit-config `ruff==0.5.1` -> `ruff==0.13.3`

Update Makefile lint for ruff to show fixes

Add `make outdated` into Makefile

Use `pathlib` instead of `os.path`

Bump `redis[hiredis]>=5.0.1,<5.3.0` -> `redis[hiredis]>=6.2.0,<7`

Bump `cryptography>=43.0.0` -> `cryptography>=46.0.0` due to security reasons

Bump `pytz~=2023.3` -> `pytz~=2025.2`

Bump `pycryptodomex~=3.19.0` -> `pycryptodomex~=3.23.0` due to security reasons

Bump linting and formatting tools
2025-10-03 09:03:30 +00:00

141 lines
3.6 KiB
Python

from __future__ import annotations
import gettext
from contextlib import contextmanager
from contextvars import ContextVar
from pathlib import Path
from typing import TYPE_CHECKING
from aiogram.utils.i18n.lazy_proxy import LazyProxy
from aiogram.utils.mixins import ContextInstanceMixin
if TYPE_CHECKING:
from collections.abc import Generator
class I18n(ContextInstanceMixin["I18n"]):
def __init__(
self,
*,
path: str | Path,
default_locale: str = "en",
domain: str = "messages",
) -> None:
self.path = Path(path)
self.default_locale = default_locale
self.domain = domain
self.ctx_locale = ContextVar("aiogram_ctx_locale", default=default_locale)
self.locales = self.find_locales()
@property
def current_locale(self) -> str:
return self.ctx_locale.get()
@current_locale.setter
def current_locale(self, value: str) -> None:
self.ctx_locale.set(value)
@contextmanager
def use_locale(self, locale: str) -> Generator[None, None, None]:
"""
Create context with specified locale
"""
ctx_token = self.ctx_locale.set(locale)
try:
yield
finally:
self.ctx_locale.reset(ctx_token)
@contextmanager
def context(self) -> Generator[I18n, None, None]:
"""
Use I18n context
"""
token = self.set_current(self)
try:
yield self
finally:
self.reset_current(token)
def find_locales(self) -> dict[str, gettext.GNUTranslations]:
"""
Load all compiled locales from path
:return: dict with locales
"""
translations: dict[str, gettext.GNUTranslations] = {}
for name in self.path.iterdir():
if not (self.path / name).is_dir():
continue
mo_path = self.path / name / "LC_MESSAGES" / (self.domain + ".mo")
if mo_path.exists():
with mo_path.open("rb") as fp:
translations[name.name] = gettext.GNUTranslations(fp)
elif mo_path.with_suffix(".po").exists(): # pragma: no cover
msg = f"Found locale '{name.name}' but this language is not compiled!"
raise RuntimeError(msg)
return translations
def reload(self) -> None:
"""
Hot reload locales
"""
self.locales = self.find_locales()
@property
def available_locales(self) -> tuple[str, ...]:
"""
list of loaded locales
:return:
"""
return tuple(self.locales.keys())
def gettext(
self,
singular: str,
plural: str | None = None,
n: int = 1,
locale: str | None = None,
) -> str:
"""
Get text
:param singular:
:param plural:
:param n:
:param locale:
:return:
"""
if locale is None:
locale = self.current_locale
if locale not in self.locales:
if n == 1:
return singular
return plural or singular
translator = self.locales[locale]
if plural is None:
return translator.gettext(singular)
return translator.ngettext(singular, plural, n)
def lazy_gettext(
self,
singular: str,
plural: str | None = None,
n: int = 1,
locale: str | None = None,
) -> LazyProxy:
return LazyProxy(
self.gettext,
singular=singular,
plural=plural,
n=n,
locale=locale,
enable_cache=False,
)