mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
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
141 lines
3.6 KiB
Python
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,
|
|
)
|