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.
- aiogram_i18n-1.5/PKG-INFO +115 -0
- aiogram_i18n-1.5/README.md +72 -0
- aiogram_i18n-1.5/pyproject.toml +204 -0
- aiogram_i18n-1.5/src/aiogram_i18n/__init__.py +15 -0
- aiogram_i18n-1.5/src/aiogram_i18n/__main__.py +4 -0
- aiogram_i18n-1.5/src/aiogram_i18n/__version__.py +3 -0
- aiogram_i18n-1.5/src/aiogram_i18n/context.py +72 -0
- aiogram_i18n-1.5/src/aiogram_i18n/cores/__init__.py +32 -0
- aiogram_i18n-1.5/src/aiogram_i18n/cores/base.py +109 -0
- aiogram_i18n-1.5/src/aiogram_i18n/cores/fluent_compile_core.py +70 -0
- aiogram_i18n-1.5/src/aiogram_i18n/cores/fluent_runtime_core.py +81 -0
- aiogram_i18n-1.5/src/aiogram_i18n/cores/gnu_text_core.py +85 -0
- aiogram_i18n-1.5/src/aiogram_i18n/cores/jinja2_core.py +56 -0
- aiogram_i18n-1.5/src/aiogram_i18n/exceptions.py +92 -0
- aiogram_i18n-1.5/src/aiogram_i18n/lazy/__init__.py +9 -0
- aiogram_i18n-1.5/src/aiogram_i18n/lazy/base.py +15 -0
- aiogram_i18n-1.5/src/aiogram_i18n/lazy/factory.py +24 -0
- aiogram_i18n-1.5/src/aiogram_i18n/lazy/filter.py +25 -0
- aiogram_i18n-1.5/src/aiogram_i18n/lazy/proxy.py +92 -0
- aiogram_i18n-1.5/src/aiogram_i18n/managers/__init__.py +9 -0
- aiogram_i18n-1.5/src/aiogram_i18n/managers/base.py +51 -0
- aiogram_i18n-1.5/src/aiogram_i18n/managers/const.py +12 -0
- aiogram_i18n-1.5/src/aiogram_i18n/managers/fsm.py +28 -0
- aiogram_i18n-1.5/src/aiogram_i18n/managers/memory.py +23 -0
- aiogram_i18n-1.5/src/aiogram_i18n/managers/redis.py +39 -0
- aiogram_i18n-1.5/src/aiogram_i18n/middleware.py +129 -0
- aiogram_i18n-1.5/src/aiogram_i18n/py.typed +0 -0
- aiogram_i18n-1.5/src/aiogram_i18n/types.py +1294 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/__init__.py +0 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/attrdict.py +19 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/__init__.py +11 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/base.py +6 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/echo.py +14 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/extract.py +81 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/multiple_extract.py +87 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/cli/stub.py +48 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/context_instance.py +64 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/fluent_stub/__init__.py +19 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/gnutext_stub/__init__.py +27 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/gnutext_stub/parser.py +48 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/language_inline_keyboard.py +74 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/magic_proxy.py +23 -0
- aiogram_i18n-1.5/src/aiogram_i18n/utils/stub_tree.py +202 -0
- 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,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
|