aiogram_i18n 1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. aiogram_i18n-1.5/PKG-INFO +115 -0
  2. aiogram_i18n-1.5/README.md +72 -0
  3. aiogram_i18n-1.5/pyproject.toml +204 -0
  4. aiogram_i18n-1.5/src/aiogram_i18n/__init__.py +15 -0
  5. aiogram_i18n-1.5/src/aiogram_i18n/__main__.py +4 -0
  6. aiogram_i18n-1.5/src/aiogram_i18n/__version__.py +3 -0
  7. aiogram_i18n-1.5/src/aiogram_i18n/context.py +72 -0
  8. aiogram_i18n-1.5/src/aiogram_i18n/cores/__init__.py +32 -0
  9. aiogram_i18n-1.5/src/aiogram_i18n/cores/base.py +109 -0
  10. aiogram_i18n-1.5/src/aiogram_i18n/cores/fluent_compile_core.py +70 -0
  11. aiogram_i18n-1.5/src/aiogram_i18n/cores/fluent_runtime_core.py +81 -0
  12. aiogram_i18n-1.5/src/aiogram_i18n/cores/gnu_text_core.py +85 -0
  13. aiogram_i18n-1.5/src/aiogram_i18n/cores/jinja2_core.py +56 -0
  14. aiogram_i18n-1.5/src/aiogram_i18n/exceptions.py +92 -0
  15. aiogram_i18n-1.5/src/aiogram_i18n/lazy/__init__.py +9 -0
  16. aiogram_i18n-1.5/src/aiogram_i18n/lazy/base.py +15 -0
  17. aiogram_i18n-1.5/src/aiogram_i18n/lazy/factory.py +24 -0
  18. aiogram_i18n-1.5/src/aiogram_i18n/lazy/filter.py +25 -0
  19. aiogram_i18n-1.5/src/aiogram_i18n/lazy/proxy.py +92 -0
  20. aiogram_i18n-1.5/src/aiogram_i18n/managers/__init__.py +9 -0
  21. aiogram_i18n-1.5/src/aiogram_i18n/managers/base.py +51 -0
  22. aiogram_i18n-1.5/src/aiogram_i18n/managers/const.py +12 -0
  23. aiogram_i18n-1.5/src/aiogram_i18n/managers/fsm.py +28 -0
  24. aiogram_i18n-1.5/src/aiogram_i18n/managers/memory.py +23 -0
  25. aiogram_i18n-1.5/src/aiogram_i18n/managers/redis.py +39 -0
  26. aiogram_i18n-1.5/src/aiogram_i18n/middleware.py +129 -0
  27. aiogram_i18n-1.5/src/aiogram_i18n/py.typed +0 -0
  28. aiogram_i18n-1.5/src/aiogram_i18n/types.py +1294 -0
  29. aiogram_i18n-1.5/src/aiogram_i18n/utils/__init__.py +0 -0
  30. aiogram_i18n-1.5/src/aiogram_i18n/utils/attrdict.py +19 -0
  31. aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/__init__.py +11 -0
  32. aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/base.py +6 -0
  33. aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/echo.py +14 -0
  34. aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/extract.py +81 -0
  35. aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/multiple_extract.py +87 -0
  36. aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/stub.py +48 -0
  37. aiogram_i18n-1.5/src/aiogram_i18n/utils/context_instance.py +64 -0
  38. aiogram_i18n-1.5/src/aiogram_i18n/utils/fluent_stub/__init__.py +19 -0
  39. aiogram_i18n-1.5/src/aiogram_i18n/utils/gnutext_stub/__init__.py +27 -0
  40. aiogram_i18n-1.5/src/aiogram_i18n/utils/gnutext_stub/parser.py +48 -0
  41. aiogram_i18n-1.5/src/aiogram_i18n/utils/language_inline_keyboard.py +74 -0
  42. aiogram_i18n-1.5/src/aiogram_i18n/utils/magic_proxy.py +23 -0
  43. aiogram_i18n-1.5/src/aiogram_i18n/utils/stub_tree.py +202 -0
  44. aiogram_i18n-1.5/src/aiogram_i18n/utils/text_decorator.py +147 -0
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiogram_i18n
3
+ Version: 1.5
4
+ Summary: Translation tool for aiogram
5
+ Keywords: telegram,bot,api,framework,wrapper,asyncio,i18n,aiogram,fluent,gnutext
6
+ Author: RootShinobi
7
+ License-Expression: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Typing :: Typed
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: aiogram>=3.0.0b8
17
+ Requires-Dist: click==8.*
18
+ Requires-Dist: fluent-compiler~=1.1 ; extra == 'compiler'
19
+ Requires-Dist: isort==8.0.1 ; extra == 'dev'
20
+ Requires-Dist: pre-commit==4.5.1 ; extra == 'dev'
21
+ Requires-Dist: ruff==0.15.5 ; extra == 'dev'
22
+ Requires-Dist: mypy==1.19.1 ; extra == 'dev'
23
+ Requires-Dist: types-polib==1.2.0.20250401 ; extra == 'dev'
24
+ Requires-Dist: sphinx==8.1.3 ; extra == 'docs'
25
+ Requires-Dist: furo==2025.12.19 ; extra == 'docs'
26
+ Requires-Dist: sphinx-autobuild==2024.10.3 ; extra == 'docs'
27
+ Requires-Dist: jinja2~=3.1.4 ; extra == 'jinja2'
28
+ Requires-Dist: fluent-runtime~=0.4.0 ; extra == 'runtime'
29
+ Requires-Dist: pytest==8.4.2 ; extra == 'test'
30
+ Requires-Dist: pytest-cov==7.0.0 ; extra == 'test'
31
+ Requires-Dist: pytest-asyncio==1.2.0 ; extra == 'test'
32
+ Requires-Dist: fluent-runtime==0.4.0 ; extra == 'test'
33
+ Requires-Dist: coverage==7.13.4 ; extra == 'test'
34
+ Requires-Python: >=3.10
35
+ Project-URL: Repository, https://github.com/aiogram/aiogram_i18n
36
+ Provides-Extra: compiler
37
+ Provides-Extra: dev
38
+ Provides-Extra: docs
39
+ Provides-Extra: jinja2
40
+ Provides-Extra: runtime
41
+ Provides-Extra: test
42
+ Description-Content-Type: text/markdown
43
+
44
+ # aiogram_i18n
45
+
46
+ Installation:
47
+ ```pip install aiogram_i18n```
48
+
49
+ To use FluentCompileCore:
50
+ ```pip install fluent_compiler```
51
+
52
+ To use FluentRuntimeCore:
53
+ ```pip install fluent.runtime```
54
+
55
+ ```python
56
+ import asyncio
57
+ from contextlib import suppress
58
+ from logging import basicConfig, INFO
59
+ from typing import Any
60
+
61
+ from aiogram import Router, Dispatcher, Bot
62
+ from aiogram.client.default import DefaultBotProperties
63
+ from aiogram.enums import ParseMode
64
+ from aiogram.filters import CommandStart
65
+ from aiogram.types import Message
66
+
67
+ from aiogram_i18n import I18nContext, LazyProxy, I18nMiddleware, LazyFilter
68
+ from aiogram_i18n.cores.fluent_runtime_core import FluentRuntimeCore
69
+ from aiogram_i18n.types import (
70
+ ReplyKeyboardMarkup, KeyboardButton
71
+ # you should import mutable objects from here if you want to use LazyProxy in them
72
+ )
73
+
74
+ router = Router(name=__name__)
75
+ rkb = ReplyKeyboardMarkup(
76
+ keyboard=[
77
+ [KeyboardButton(text=LazyProxy("help"))] # or L.help()
78
+ ], resize_keyboard=True
79
+ )
80
+
81
+
82
+ @router.message(CommandStart())
83
+ async def cmd_start(message: Message, i18n: I18nContext) -> Any:
84
+ name = message.from_user.mention_html()
85
+ return message.reply(
86
+ text=i18n.get("hello", user=name), # or i18n.hello(user=name)
87
+ reply_markup=rkb
88
+ )
89
+
90
+
91
+ @router.message(LazyFilter("help")) # or LazyProxy("help") or F.text == LazyProxy("help")
92
+ async def cmd_help(message: Message) -> Any:
93
+ return message.reply(text="-- " + message.text + " --")
94
+
95
+
96
+ async def main() -> None:
97
+ basicConfig(level=INFO)
98
+ bot = Bot("42:ABC", default=DefaultBotProperties(parse_mode=ParseMode.HTML))
99
+ i18n_middleware = I18nMiddleware(
100
+ core=FluentRuntimeCore(
101
+ path="locales/{locale}/LC_MESSAGES"
102
+ )
103
+ )
104
+
105
+ dp = Dispatcher()
106
+ dp.include_router(router)
107
+ i18n_middleware.setup(dispatcher=dp)
108
+
109
+ await dp.start_polling(bot)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ with suppress(KeyboardInterrupt):
114
+ asyncio.run(main())
115
+ ```
@@ -0,0 +1,72 @@
1
+ # aiogram_i18n
2
+
3
+ Installation:
4
+ ```pip install aiogram_i18n```
5
+
6
+ To use FluentCompileCore:
7
+ ```pip install fluent_compiler```
8
+
9
+ To use FluentRuntimeCore:
10
+ ```pip install fluent.runtime```
11
+
12
+ ```python
13
+ import asyncio
14
+ from contextlib import suppress
15
+ from logging import basicConfig, INFO
16
+ from typing import Any
17
+
18
+ from aiogram import Router, Dispatcher, Bot
19
+ from aiogram.client.default import DefaultBotProperties
20
+ from aiogram.enums import ParseMode
21
+ from aiogram.filters import CommandStart
22
+ from aiogram.types import Message
23
+
24
+ from aiogram_i18n import I18nContext, LazyProxy, I18nMiddleware, LazyFilter
25
+ from aiogram_i18n.cores.fluent_runtime_core import FluentRuntimeCore
26
+ from aiogram_i18n.types import (
27
+ ReplyKeyboardMarkup, KeyboardButton
28
+ # you should import mutable objects from here if you want to use LazyProxy in them
29
+ )
30
+
31
+ router = Router(name=__name__)
32
+ rkb = ReplyKeyboardMarkup(
33
+ keyboard=[
34
+ [KeyboardButton(text=LazyProxy("help"))] # or L.help()
35
+ ], resize_keyboard=True
36
+ )
37
+
38
+
39
+ @router.message(CommandStart())
40
+ async def cmd_start(message: Message, i18n: I18nContext) -> Any:
41
+ name = message.from_user.mention_html()
42
+ return message.reply(
43
+ text=i18n.get("hello", user=name), # or i18n.hello(user=name)
44
+ reply_markup=rkb
45
+ )
46
+
47
+
48
+ @router.message(LazyFilter("help")) # or LazyProxy("help") or F.text == LazyProxy("help")
49
+ async def cmd_help(message: Message) -> Any:
50
+ return message.reply(text="-- " + message.text + " --")
51
+
52
+
53
+ async def main() -> None:
54
+ basicConfig(level=INFO)
55
+ bot = Bot("42:ABC", default=DefaultBotProperties(parse_mode=ParseMode.HTML))
56
+ i18n_middleware = I18nMiddleware(
57
+ core=FluentRuntimeCore(
58
+ path="locales/{locale}/LC_MESSAGES"
59
+ )
60
+ )
61
+
62
+ dp = Dispatcher()
63
+ dp.include_router(router)
64
+ i18n_middleware.setup(dispatcher=dp)
65
+
66
+ await dp.start_polling(bot)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ with suppress(KeyboardInterrupt):
71
+ asyncio.run(main())
72
+ ```
@@ -0,0 +1,204 @@
1
+ [project]
2
+ name = "aiogram_i18n"
3
+ version = "1.5"
4
+ authors = [
5
+ { name = "RootShinobi" },
6
+ ]
7
+ description = "Translation tool for aiogram"
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ requires-python = ">=3.10"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Operating System :: OS Independent",
14
+ "Typing :: Typed",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ ]
21
+ keywords = [
22
+ "telegram",
23
+ "bot",
24
+ "api",
25
+ "framework",
26
+ "wrapper",
27
+ "asyncio",
28
+ "i18n",
29
+ "aiogram",
30
+ "fluent",
31
+ "gnutext",
32
+ ]
33
+ dependencies = [
34
+ "aiogram>=3.0.0b8",
35
+ "click==8.*",
36
+ ]
37
+
38
+ [tool.uv]
39
+ package = true
40
+
41
+ [build-system]
42
+ requires = ["uv_build>=0.10.7,<0.11.0"]
43
+ build-backend = "uv_build"
44
+
45
+ [project.urls]
46
+ Repository = "https://github.com/aiogram/aiogram_i18n"
47
+
48
+ [project.scripts]
49
+ i18n = "aiogram_i18n.__main__:main"
50
+
51
+ [project.optional-dependencies]
52
+ compiler = [
53
+ "fluent_compiler~=1.1",
54
+ ]
55
+ runtime = [
56
+ "fluent.runtime~=0.4.0",
57
+ ]
58
+ jinja2 = [
59
+ "jinja2~=3.1.4",
60
+ ]
61
+ test = [
62
+ "pytest==8.4.2",
63
+ "pytest-cov==7.0.0",
64
+ "pytest-asyncio==1.2.0",
65
+ "fluent.runtime==0.4.0",
66
+ "coverage==7.13.4",
67
+ ]
68
+ dev = [
69
+ "isort==8.0.1",
70
+ "pre-commit==4.5.1",
71
+ "ruff==0.15.5",
72
+ "mypy==1.19.1",
73
+ "types-polib==1.2.0.20250401",
74
+ ]
75
+ docs = [
76
+ "sphinx==8.1.3",
77
+ "furo==2025.12.19",
78
+ "sphinx-autobuild==2024.10.3",
79
+ ]
80
+
81
+ [tool.hatch.envs.dev]
82
+ post-install = [
83
+ "pre-commit install",
84
+ ]
85
+
86
+ features = [
87
+ "compiler",
88
+ "runtime",
89
+ "jinja2",
90
+ "dev",
91
+ "test",
92
+ "docs",
93
+ ]
94
+
95
+ [tool.isort]
96
+ py_version = 310
97
+ line_length = 99
98
+ multi_line_output = 3
99
+ force_grid_wrap = 0
100
+ include_trailing_comma = true
101
+ split_on_trailing_comma = false
102
+ single_line_exclusions = ["."]
103
+ sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
104
+ skip_gitignore = true
105
+ extend_skip = ["__pycache__"]
106
+
107
+ [tool.ruff]
108
+ src = ["src", "tests"]
109
+ line-length = 99
110
+ exclude = [
111
+ ".git",
112
+ ".mypy_cache",
113
+ ".ruff_cache",
114
+ "__pypackages__",
115
+ "__pycache__",
116
+ "venv",
117
+ ".venv",
118
+ "tests/.files",
119
+ "tests/files/py",
120
+ "dist",
121
+ "build",
122
+ ]
123
+
124
+ [tool.ruff.lint]
125
+ select = ["ALL"]
126
+ ignore = [
127
+ "A002",
128
+ "ANN001", "ANN003", "ANN204", "ANN401",
129
+ "COM812",
130
+ "CPY001",
131
+ "D",
132
+ "DOC",
133
+ "FBT001", "FBT002",
134
+ "N805",
135
+ "PLC0415",
136
+ "PLR0913", "PLR0917", "PLR1702", "PLR6301",
137
+ "PLW2901",
138
+ "RUF052", "RUF067",
139
+ "TC006",
140
+ "TID252",
141
+ ]
142
+ unfixable = []
143
+ exclude = [
144
+ "examples"
145
+ ]
146
+
147
+ [tool.ruff.lint.flake8-tidy-imports]
148
+ ban-relative-imports = "all"
149
+
150
+ [tool.ruff.lint.per-file-ignores]
151
+ # Tests can use magic values, assertions, and relative imports
152
+ "tests/**/*" = ["PLR2004", "S101", "TID252"]
153
+
154
+ [tool.ruff.format]
155
+ quote-style = "double"
156
+ indent-style = "space"
157
+ skip-magic-trailing-comma = false
158
+ line-ending = "auto"
159
+
160
+ [tool.mypy]
161
+ mypy_path = "src"
162
+ packages = ["aiogram_i18n"]
163
+ plugins = ["pydantic.mypy"]
164
+ allow_redefinition = true
165
+ check_untyped_defs = true
166
+ disallow_any_generics = true
167
+ disallow_incomplete_defs = true
168
+ disallow_untyped_calls = true
169
+ disallow_untyped_defs = true
170
+ extra_checks = true
171
+ follow_imports = "skip"
172
+ follow_imports_for_stubs = false
173
+ ignore_missing_imports = false
174
+ namespace_packages = true
175
+ no_implicit_optional = true
176
+ no_implicit_reexport = true
177
+ pretty = true
178
+ show_absolute_path = true
179
+ show_error_codes = true
180
+ show_error_context = true
181
+ warn_unused_configs = true
182
+ warn_unused_ignores = true
183
+ disable_error_code = [
184
+ "no-redef",
185
+ ]
186
+ exclude = [
187
+ "\\.?venv",
188
+ "\\.idea",
189
+ "\\.tests?",
190
+ ]
191
+
192
+ [[tool.mypy.overrides]]
193
+ module = "mre"
194
+ strict_optional = false
195
+
196
+ [[tool.mypy.overrides]]
197
+ module = "aiogram_i18n.types"
198
+ disable_error_code = ["arg-type", "assignment"]
199
+
200
+ [[tool.mypy.overrides]]
201
+ module = [
202
+ "redis.*", "fluent_compiler.*"
203
+ ]
204
+ ignore_missing_imports = true
@@ -0,0 +1,15 @@
1
+ from .__version__ import __version__
2
+ from .context import I18nContext
3
+ from .lazy import LazyFactory, LazyFilter, LazyProxy
4
+ from .middleware import I18nMiddleware
5
+
6
+ L = LazyFactory()
7
+
8
+ __all__ = (
9
+ "I18nContext",
10
+ "I18nMiddleware",
11
+ "L",
12
+ "LazyFilter",
13
+ "LazyProxy",
14
+ "__version__",
15
+ )
@@ -0,0 +1,4 @@
1
+ from aiogram_i18n.utils.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version(__package__)
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from aiogram_i18n.utils.context_instance import ContextInstanceMixin
7
+ from aiogram_i18n.utils.magic_proxy import MagicProxy
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Generator
11
+
12
+ from aiogram_i18n.cores.base import BaseCore
13
+ from aiogram_i18n.managers.base import BaseManager
14
+
15
+
16
+ class I18nContext(ContextInstanceMixin["I18nContext"]):
17
+ locale: str
18
+ core: BaseCore[Any]
19
+ manager: BaseManager
20
+ data: dict[str, Any]
21
+ key_separator: str
22
+ context: dict[str, Any]
23
+
24
+ def __init__(
25
+ self,
26
+ locale: str,
27
+ core: BaseCore[Any],
28
+ manager: BaseManager,
29
+ data: dict[str, Any],
30
+ key_separator: str = "-",
31
+ ) -> None:
32
+ self.locale = locale
33
+ self.core = core
34
+ self.manager = manager
35
+ self.data = data
36
+ self.key_separator = key_separator
37
+ self.context = {}
38
+
39
+ def get(self, key: str, locale: str | None = None, /, **kwargs: Any) -> str:
40
+ return self.core.get(key, locale or self.locale, **kwargs)
41
+
42
+ __call__ = get
43
+
44
+ def __getattr__(self, item: str) -> MagicProxy[str]:
45
+ proxy = MagicProxy(self, key_separator=self.key_separator)
46
+ return proxy.__getattr__(item)
47
+
48
+ async def set_locale(self, locale: str, **kwargs: Any) -> None:
49
+ kwargs.update(self.data, core=self.core)
50
+ await self.manager.locale_setter(locale, **kwargs)
51
+ self.locale = locale
52
+
53
+ @contextmanager
54
+ def use_locale(self, locale: str) -> Generator[I18nContext, None, None]:
55
+ old_locale = self.locale
56
+ self.locale = locale
57
+ try:
58
+ yield self
59
+ finally:
60
+ self.locale = old_locale
61
+
62
+ @contextmanager
63
+ def use_context(self, **kwargs: Any) -> Generator[I18nContext, None, None]:
64
+ old_context = self.context
65
+ self.context = kwargs
66
+ try:
67
+ yield self
68
+ finally:
69
+ self.context = old_context
70
+
71
+ def set_context(self, **kwargs: Any) -> None:
72
+ self.context = kwargs
@@ -0,0 +1,32 @@
1
+ from importlib import import_module
2
+ from typing import TYPE_CHECKING, Any, cast
3
+
4
+ from .base import BaseCore
5
+
6
+ if TYPE_CHECKING:
7
+ from .fluent_compile_core import FluentCompileCore
8
+ from .fluent_runtime_core import FluentRuntimeCore
9
+ from .gnu_text_core import GNUTextCore
10
+ from .jinja2_core import Jinja2Core
11
+
12
+
13
+ __cores__ = {
14
+ "GNUTextCore": ".gnu_text_core",
15
+ "FluentRuntimeCore": ".fluent_runtime_core",
16
+ "FluentCompileCore": ".fluent_compile_core",
17
+ "Jinja2Core": ".jinja2_core",
18
+ }
19
+
20
+ __all__ = (
21
+ "BaseCore",
22
+ "FluentCompileCore",
23
+ "FluentRuntimeCore",
24
+ "GNUTextCore",
25
+ "Jinja2Core",
26
+ )
27
+
28
+
29
+ def __getattr__(name: str) -> BaseCore[Any]:
30
+ if name not in __all__:
31
+ raise AttributeError
32
+ return cast(BaseCore[Any], getattr(import_module(__cores__[name], "aiogram_i18n.cores"), name))
@@ -0,0 +1,109 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import Any, Generic, TypeVar, cast
4
+
5
+ from aiogram_i18n import I18nContext
6
+ from aiogram_i18n.exceptions import NoLocalesError, NoLocalesFoundError, NoTranslateFileExistsError
7
+
8
+ Translator = TypeVar("Translator")
9
+
10
+
11
+ class BaseCore(ABC, Generic[Translator]):
12
+ """
13
+ Is an abstract base class for implementing core functionality for translation.
14
+ """
15
+
16
+ default_locale: str | None
17
+ locales: dict[str, Translator]
18
+ locales_map: dict[str, str]
19
+
20
+ def __init__(
21
+ self,
22
+ path: str | Path,
23
+ default_locale: str | None = None,
24
+ locales_map: dict[str, str] | None = None,
25
+ ) -> None:
26
+ """
27
+
28
+ :param default_locale: The default locale to be used for translations.
29
+ If not provided, it will default to None.
30
+ """
31
+ self.path = path if isinstance(path, Path) else Path(path)
32
+ self.default_locale = default_locale
33
+ self.locales = {}
34
+ self.locales_map = locales_map or {}
35
+
36
+ @abstractmethod
37
+ def get(self, message: str, locale: str | None = None, /, **kwargs: Any) -> str:
38
+ pass
39
+
40
+ def nget(
41
+ self,
42
+ singular: str,
43
+ plural: str | None = None, # noqa: ARG002
44
+ n: int = 1, # noqa: ARG002
45
+ locale: str | None = None,
46
+ /,
47
+ **kwargs: Any,
48
+ ) -> str:
49
+ return self.get(singular, locale, **kwargs)
50
+
51
+ def get_translator(self, locale: str) -> Translator:
52
+ return self.locales[locale]
53
+
54
+ def get_locale(self, locale: str | None = None) -> str:
55
+ if locale is None:
56
+ locale = I18nContext.get_current(no_error=False).locale
57
+ if locale not in self.locales:
58
+ locale = cast(str, self.default_locale)
59
+ return locale
60
+
61
+ async def startup(self) -> None:
62
+ self.locales.update(self.find_locales())
63
+
64
+ async def shutdown(self) -> None:
65
+ self.locales.clear()
66
+
67
+ @staticmethod
68
+ def _extract_locales(path: Path) -> list[str]:
69
+ if "{locale}" in path.parts:
70
+ path = Path(*path.parts[: path.parts.index("{locale}")])
71
+
72
+ locales: list[str] = [file_path.name for file_path in path.iterdir() if file_path.is_dir()]
73
+
74
+ if not locales:
75
+ raise NoLocalesFoundError(locales=[], path=path.as_posix())
76
+
77
+ return locales
78
+
79
+ @staticmethod
80
+ def _find_locales(
81
+ path: Path, locales: list[str], ext: str | None = None
82
+ ) -> dict[str, list[Path]]:
83
+ if not locales:
84
+ raise NoLocalesError
85
+
86
+ paths: dict[str, list[Path]] = {}
87
+
88
+ if "{locale}" not in path.as_posix():
89
+ path = path.joinpath("{locale}")
90
+
91
+ for locale in locales:
92
+ locale_path = Path(path.as_posix().format(locale=locale))
93
+ recursive_paths = locale_path.rglob(f"*{ext}") # Will recursively search for files
94
+ paths.setdefault(locale, []).extend(recursive_paths)
95
+
96
+ if not paths[locale]:
97
+ raise NoTranslateFileExistsError(ext=ext, locale_path=locale_path.as_posix())
98
+ if not paths:
99
+ raise NoLocalesFoundError(locales=locales, path=path.as_posix())
100
+
101
+ return paths
102
+
103
+ @abstractmethod
104
+ def find_locales(self) -> dict[str, Translator]:
105
+ pass
106
+
107
+ @property
108
+ def available_locales(self) -> tuple[str, ...]:
109
+ return tuple(self.locales.keys())
@@ -0,0 +1,70 @@
1
+ from collections.abc import Callable
2
+ from pathlib import Path
3
+ from typing import Any, cast
4
+
5
+ from aiogram_i18n.exceptions import FluentMessageError, KeyNotFoundError, NoModuleError
6
+ from aiogram_i18n.utils.text_decorator import td
7
+
8
+ try:
9
+ from fluent_compiler.bundle import FluentBundle
10
+ except ImportError as e:
11
+ raise NoModuleError(name="FluentCompileCore", module_name="fluent_compiler") from e
12
+
13
+ from aiogram_i18n.cores.base import BaseCore
14
+
15
+
16
+ class FluentCompileCore(BaseCore[FluentBundle]):
17
+ def __init__(
18
+ self,
19
+ path: str | Path,
20
+ default_locale: str | None = None,
21
+ use_isolating: bool = False,
22
+ functions: dict[str, Callable[..., Any]] | None = None,
23
+ raise_key_error: bool = True,
24
+ use_td: bool = True,
25
+ locales_map: dict[str, str] | None = None,
26
+ ) -> None:
27
+ super().__init__(path=path, default_locale=default_locale, locales_map=locales_map)
28
+ self.use_isolating = use_isolating
29
+ self.functions = functions or {}
30
+ if use_td:
31
+ self.functions.update(td.functions)
32
+ self.raise_key_error = raise_key_error
33
+
34
+ def get(self, message: str, locale: str | None = None, /, **kwargs: Any) -> str:
35
+ locale = self.get_locale(locale=locale)
36
+ translator: FluentBundle = self.get_translator(locale=locale)
37
+ try:
38
+ text, errors = translator.format(message_id=message, args=kwargs)
39
+ except KeyError:
40
+ locale: str | None = self.locales_map.get(locale)
41
+ if locale is not None:
42
+ return self.get(message, locale, **kwargs)
43
+ if self.raise_key_error:
44
+ raise KeyNotFoundError(message) from None
45
+ return message
46
+ if errors:
47
+ raise FluentMessageError(message_id=message, errors=errors)
48
+ return cast(str, text) # 'cause fluent_compiler type-ignored
49
+
50
+ def find_locales(self) -> dict[str, FluentBundle]:
51
+ """
52
+ Load all compiled locales from path
53
+
54
+ :return: dict with locales
55
+ """
56
+ translations: dict[str, FluentBundle] = {}
57
+ locales = self._extract_locales(self.path)
58
+ for locale, paths in self._find_locales(self.path, locales, ".ftl").items():
59
+ texts = []
60
+ for path in paths:
61
+ with path.open("r", encoding="utf8") as fp:
62
+ texts.append(fp.read())
63
+ translations[locale] = FluentBundle.from_string(
64
+ text="\n".join(texts),
65
+ locale=locale,
66
+ use_isolating=self.use_isolating,
67
+ functions=self.functions,
68
+ )
69
+
70
+ return translations