fastapi-rosetta 0.1.0__py3-none-any.whl
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.
- fastapi_rosetta/__init__.py +20 -0
- fastapi_rosetta/access.py +104 -0
- fastapi_rosetta/conf/__init__.py +130 -0
- fastapi_rosetta/dependencies.py +34 -0
- fastapi_rosetta/jinja.py +256 -0
- fastapi_rosetta/locale/ar/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/ar/LC_MESSAGES/messages.po +209 -0
- fastapi_rosetta/locale/az/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/az/LC_MESSAGES/messages.po +210 -0
- fastapi_rosetta/locale/cs/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/cs/LC_MESSAGES/messages.po +201 -0
- fastapi_rosetta/locale/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/de/LC_MESSAGES/messages.po +206 -0
- fastapi_rosetta/locale/es/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/es/LC_MESSAGES/messages.po +231 -0
- fastapi_rosetta/locale/fa/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/fa/LC_MESSAGES/messages.po +203 -0
- fastapi_rosetta/locale/fr/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/fr/LC_MESSAGES/messages.po +209 -0
- fastapi_rosetta/locale/hu/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/hu/LC_MESSAGES/messages.po +197 -0
- fastapi_rosetta/locale/it/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/it/LC_MESSAGES/messages.po +204 -0
- fastapi_rosetta/locale/ky/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/ky/LC_MESSAGES/messages.po +197 -0
- fastapi_rosetta/locale/nl/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/nl/LC_MESSAGES/messages.po +203 -0
- fastapi_rosetta/locale/pl/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/pl/LC_MESSAGES/messages.po +213 -0
- fastapi_rosetta/locale/ru/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/ru/LC_MESSAGES/messages.po +200 -0
- fastapi_rosetta/locale/tr/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/tr/LC_MESSAGES/messages.po +230 -0
- fastapi_rosetta/locale/uk/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/uk/LC_MESSAGES/messages.po +209 -0
- fastapi_rosetta/locale/xx/LC_MESSAGES/messages.po +33 -0
- fastapi_rosetta/locale/zh_Hans/LC_MESSAGES/messages.mo +0 -0
- fastapi_rosetta/locale/zh_Hans/LC_MESSAGES/messages.po +212 -0
- fastapi_rosetta/poutil.py +173 -0
- fastapi_rosetta/routers/__init__.py +3 -0
- fastapi_rosetta/routers/rosetta.py +130 -0
- fastapi_rosetta/routers/services.py +817 -0
- fastapi_rosetta/signals.py +32 -0
- fastapi_rosetta/static/rosetta/css/rosetta.css +600 -0
- fastapi_rosetta/static/rosetta/favicon.svg +4 -0
- fastapi_rosetta/static/rosetta/img/icon_searchbox_rosetta.png +0 -0
- fastapi_rosetta/static/rosetta/js/rosetta.js +255 -0
- fastapi_rosetta/storage.py +130 -0
- fastapi_rosetta/templates/rosetta/base.html +131 -0
- fastapi_rosetta/templates/rosetta/file-list.html +151 -0
- fastapi_rosetta/templates/rosetta/form.html +463 -0
- fastapi_rosetta/test_smoke.py +17 -0
- fastapi_rosetta/translate_utils.py +271 -0
- fastapi_rosetta/version.py +6 -0
- fastapi_rosetta-0.1.0.dist-info/METADATA +243 -0
- fastapi_rosetta-0.1.0.dist-info/RECORD +58 -0
- fastapi_rosetta-0.1.0.dist-info/WHEEL +4 -0
- fastapi_rosetta-0.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .conf import (
|
|
4
|
+
RosettaConfig,
|
|
5
|
+
configure,
|
|
6
|
+
get_settings,
|
|
7
|
+
reset_settings,
|
|
8
|
+
update_settings,
|
|
9
|
+
)
|
|
10
|
+
from .routers import mount_static, router
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"RosettaConfig",
|
|
14
|
+
"configure",
|
|
15
|
+
"get_settings",
|
|
16
|
+
"mount_static",
|
|
17
|
+
"reset_settings",
|
|
18
|
+
"router",
|
|
19
|
+
"update_settings",
|
|
20
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from .conf import get_settings
|
|
8
|
+
|
|
9
|
+
AccessControl = Callable[[object], bool]
|
|
10
|
+
LanguageAccessControl = Callable[[object, str], bool]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def can_translate(user: object) -> bool:
|
|
14
|
+
return get_access_control_function()(user)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def can_translate_language(
|
|
18
|
+
user: object,
|
|
19
|
+
language_code: str,
|
|
20
|
+
) -> bool:
|
|
21
|
+
settings = get_settings()
|
|
22
|
+
|
|
23
|
+
if not settings.language_groups:
|
|
24
|
+
return can_translate(user)
|
|
25
|
+
|
|
26
|
+
return get_language_access_control_function()(
|
|
27
|
+
user,
|
|
28
|
+
language_code,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_access_control_function() -> AccessControl:
|
|
33
|
+
settings = get_settings()
|
|
34
|
+
|
|
35
|
+
access_function = settings.access_control_function
|
|
36
|
+
|
|
37
|
+
if access_function is None:
|
|
38
|
+
return allow_all
|
|
39
|
+
|
|
40
|
+
if callable(access_function):
|
|
41
|
+
return cast(
|
|
42
|
+
AccessControl,
|
|
43
|
+
access_function,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if isinstance(access_function, str):
|
|
47
|
+
module_name, func_name = access_function.rsplit(".", 1)
|
|
48
|
+
|
|
49
|
+
module = importlib.import_module(module_name)
|
|
50
|
+
|
|
51
|
+
return cast(
|
|
52
|
+
AccessControl,
|
|
53
|
+
getattr(module, func_name),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
raise TypeError(
|
|
57
|
+
"access_control_function must be " "callable, import path string or None."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_language_access_control_function() -> LanguageAccessControl:
|
|
62
|
+
settings = get_settings()
|
|
63
|
+
|
|
64
|
+
access_function: Any = getattr(
|
|
65
|
+
settings,
|
|
66
|
+
"language_access_control_function",
|
|
67
|
+
None,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if access_function is None:
|
|
71
|
+
return default_language_access_control
|
|
72
|
+
|
|
73
|
+
if callable(access_function):
|
|
74
|
+
return cast(
|
|
75
|
+
LanguageAccessControl,
|
|
76
|
+
access_function,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if isinstance(access_function, str):
|
|
80
|
+
module_name, func_name = access_function.rsplit(".", 1)
|
|
81
|
+
|
|
82
|
+
module = importlib.import_module(module_name)
|
|
83
|
+
|
|
84
|
+
return cast(
|
|
85
|
+
LanguageAccessControl,
|
|
86
|
+
getattr(module, func_name),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
raise TypeError(
|
|
90
|
+
"language_access_control_function "
|
|
91
|
+
"must be callable, import path string "
|
|
92
|
+
"or None."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def allow_all(user: object) -> bool:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def default_language_access_control(
|
|
101
|
+
user: object,
|
|
102
|
+
language_code: str,
|
|
103
|
+
) -> bool:
|
|
104
|
+
return can_translate(user)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from threading import Lock
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RosettaConfig(BaseModel):
|
|
10
|
+
model_config = ConfigDict(
|
|
11
|
+
extra="forbid",
|
|
12
|
+
validate_assignment=True,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
messages_per_page: int = 10
|
|
16
|
+
|
|
17
|
+
enable_translation_suggestions: bool = False
|
|
18
|
+
|
|
19
|
+
yandex_translate_key: str | None = None
|
|
20
|
+
azure_client_secret: str | None = None
|
|
21
|
+
|
|
22
|
+
google_application_credentials_path: str | None = None
|
|
23
|
+
google_project_id: str | None = None
|
|
24
|
+
|
|
25
|
+
deepl_auth_key: str | None = None
|
|
26
|
+
|
|
27
|
+
openai_api_key: str | None = None
|
|
28
|
+
openai_prompt_template: str | None = None
|
|
29
|
+
|
|
30
|
+
main_language: str | None = None
|
|
31
|
+
|
|
32
|
+
messages_source_language_code: str = "en"
|
|
33
|
+
messages_source_language_name: str = "English"
|
|
34
|
+
|
|
35
|
+
access_control_function: str | None = None
|
|
36
|
+
|
|
37
|
+
wsgi_auto_reload: bool = False
|
|
38
|
+
uwsgi_auto_reload: bool = False
|
|
39
|
+
|
|
40
|
+
excluded_applications: tuple[str, ...] = ()
|
|
41
|
+
|
|
42
|
+
pofile_wrap_width: int = 78
|
|
43
|
+
|
|
44
|
+
storage_class: str = "fastapi_rosetta.storage.MemoryRosettaStorage"
|
|
45
|
+
|
|
46
|
+
enable_reflang: bool = False
|
|
47
|
+
|
|
48
|
+
pofilenames: tuple[str, ...] = ("messages.po",)
|
|
49
|
+
|
|
50
|
+
cache_name: str = "default"
|
|
51
|
+
|
|
52
|
+
requires_auth: bool = True
|
|
53
|
+
|
|
54
|
+
locale_paths: tuple[str, ...] = ()
|
|
55
|
+
excluded_paths: tuple[str, ...] = ()
|
|
56
|
+
|
|
57
|
+
language_groups: bool = False
|
|
58
|
+
|
|
59
|
+
auto_compile: bool = True
|
|
60
|
+
|
|
61
|
+
show_occurrences: bool = True
|
|
62
|
+
|
|
63
|
+
deepl_languages: dict[str, str] = Field(
|
|
64
|
+
default_factory=dict,
|
|
65
|
+
)
|
|
66
|
+
openai_base_url: str | None = None
|
|
67
|
+
|
|
68
|
+
openai_model: str = "gpt-4.1-mini"
|
|
69
|
+
|
|
70
|
+
languages: tuple[tuple[str, str], ...] = ()
|
|
71
|
+
|
|
72
|
+
login_url: str = "/login/"
|
|
73
|
+
|
|
74
|
+
languages_bidi: frozenset[str] = frozenset(
|
|
75
|
+
{"he", "ar", "ar-dz", "ckb", "fa", "ur"},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
static_url_prefix: str = "/rosetta/static"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_lock = Lock()
|
|
82
|
+
|
|
83
|
+
_settings = RosettaConfig()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def configure(config: RosettaConfig) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Configure fastapi-rosetta.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
configure(
|
|
92
|
+
RosettaConfig(
|
|
93
|
+
messages_per_page=50,
|
|
94
|
+
auto_compile=False,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
"""
|
|
98
|
+
global _settings
|
|
99
|
+
|
|
100
|
+
with _lock:
|
|
101
|
+
_settings = config
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_settings() -> RosettaConfig:
|
|
105
|
+
return _settings
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def reset_settings() -> None:
|
|
109
|
+
"""
|
|
110
|
+
Mainly useful for tests.
|
|
111
|
+
"""
|
|
112
|
+
global _settings
|
|
113
|
+
|
|
114
|
+
with _lock:
|
|
115
|
+
_settings = RosettaConfig()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def update_settings(**kwargs: Any) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Partially update existing settings.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
update_settings(auto_compile=False)
|
|
124
|
+
"""
|
|
125
|
+
global _settings
|
|
126
|
+
|
|
127
|
+
with _lock:
|
|
128
|
+
_settings = _settings.model_copy(
|
|
129
|
+
update=kwargs,
|
|
130
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from fastapi import Depends, HTTPException, Request
|
|
7
|
+
|
|
8
|
+
from .access import can_translate
|
|
9
|
+
from .conf import get_settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AnonymousUser:
|
|
13
|
+
first_name = "Anonymous"
|
|
14
|
+
last_name = "User"
|
|
15
|
+
email = "anonymous@user.tld"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_user(request: Request) -> object:
|
|
19
|
+
return getattr(request.state, "user", AnonymousUser())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def require_translate(
|
|
23
|
+
request: Request,
|
|
24
|
+
user: Annotated[object, Depends(get_user)],
|
|
25
|
+
) -> object:
|
|
26
|
+
settings = get_settings()
|
|
27
|
+
if settings.requires_auth and not can_translate(user):
|
|
28
|
+
login_url = settings.login_url
|
|
29
|
+
next_url = quote(str(request.url), safe="")
|
|
30
|
+
raise HTTPException(
|
|
31
|
+
status_code=302,
|
|
32
|
+
headers={"Location": f"{login_url}?next={next_url}"},
|
|
33
|
+
)
|
|
34
|
+
return user
|
fastapi_rosetta/jinja.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gettext
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
12
|
+
from jinja2.runtime import Undefined
|
|
13
|
+
from markupsafe import Markup, escape
|
|
14
|
+
|
|
15
|
+
from .access import can_translate
|
|
16
|
+
from .conf import RosettaConfig, get_settings
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"TabIndexCounter",
|
|
20
|
+
"get_jinja_environment",
|
|
21
|
+
"register_jinja_filters",
|
|
22
|
+
"render_template",
|
|
23
|
+
"RosettaSettingsView",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
RX = re.compile(r"(%(\([^\s\)]*\))?[sd]|\{[\w\d_]+?\})")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@lru_cache
|
|
31
|
+
def get_translation(
|
|
32
|
+
language: str,
|
|
33
|
+
) -> gettext.GNUTranslations | gettext.NullTranslations:
|
|
34
|
+
|
|
35
|
+
settings = get_settings()
|
|
36
|
+
|
|
37
|
+
locale_paths = [
|
|
38
|
+
Path(__file__).parent / "locale",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
if settings.locale_paths:
|
|
42
|
+
locale_paths.extend(Path(path) for path in settings.locale_paths)
|
|
43
|
+
|
|
44
|
+
for locale_path in locale_paths:
|
|
45
|
+
try:
|
|
46
|
+
return gettext.translation(
|
|
47
|
+
"messages",
|
|
48
|
+
localedir=str(locale_path),
|
|
49
|
+
languages=[language],
|
|
50
|
+
)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
return gettext.NullTranslations()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def gettext_lazy(
|
|
58
|
+
language: str,
|
|
59
|
+
) -> Callable[[str], str]:
|
|
60
|
+
return get_translation(language).gettext
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RosettaSettingsView:
|
|
64
|
+
"""Expose RosettaConfig fields with legacy uppercase names for templates."""
|
|
65
|
+
|
|
66
|
+
_ALIASES = {
|
|
67
|
+
"MESSAGES_PER_PAGE": "messages_per_page",
|
|
68
|
+
"ENABLE_TRANSLATION_SUGGESTIONS": "enable_translation_suggestions",
|
|
69
|
+
"YANDEX_TRANSLATE_KEY": "yandex_translate_key",
|
|
70
|
+
"AZURE_CLIENT_SECRET": "azure_client_secret",
|
|
71
|
+
"GOOGLE_APPLICATION_CREDENTIALS_PATH": ("google_application_credentials_path"),
|
|
72
|
+
"GOOGLE_PROJECT_ID": "google_project_id",
|
|
73
|
+
"DEEPL_AUTH_KEY": "deepl_auth_key",
|
|
74
|
+
"OPENAI_API_KEY": "openai_api_key",
|
|
75
|
+
"MAIN_LANGUAGE": "main_language",
|
|
76
|
+
"MESSAGES_SOURCE_LANGUAGE_CODE": "messages_source_language_code",
|
|
77
|
+
"MESSAGES_SOURCE_LANGUAGE_NAME": "messages_source_language_name",
|
|
78
|
+
"WSGI_AUTO_RELOAD": "wsgi_auto_reload",
|
|
79
|
+
"UWSGI_AUTO_RELOAD": "uwsgi_auto_reload",
|
|
80
|
+
"EXCLUDED_APPLICATIONS": "excluded_applications",
|
|
81
|
+
"POFILE_WRAP_WIDTH": "pofile_wrap_width",
|
|
82
|
+
"STORAGE_CLASS": "storage_class",
|
|
83
|
+
"ENABLE_REFLANG": "enable_reflang",
|
|
84
|
+
"POFILENAMES": "pofilenames",
|
|
85
|
+
"CACHE_NAME": "cache_name",
|
|
86
|
+
"REQUIRES_AUTH": "requires_auth",
|
|
87
|
+
"LOCALE_PATHS": "locale_paths",
|
|
88
|
+
"EXCLUDED_PATHS": "excluded_paths",
|
|
89
|
+
"LANGUAGE_GROUPS": "language_groups",
|
|
90
|
+
"AUTO_COMPILE": "auto_compile",
|
|
91
|
+
"SHOW_OCCURRENCES": "show_occurrences",
|
|
92
|
+
"DEEPL_LANGUAGES": "deepl_languages",
|
|
93
|
+
"OPENAI_BASE_URL": "openai_base_url",
|
|
94
|
+
"OPENAI_MODEL": "openai_model",
|
|
95
|
+
"ROSETTA_LANGUAGES": "languages",
|
|
96
|
+
"LOGIN_URL": "login_url",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def __init__(self, config: RosettaConfig | None = None) -> None:
|
|
100
|
+
self._config = config or get_settings()
|
|
101
|
+
|
|
102
|
+
def __getattr__(self, name: str) -> Any:
|
|
103
|
+
attr = self._ALIASES.get(name, name)
|
|
104
|
+
return getattr(self._config, attr)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TabIndexCounter:
|
|
108
|
+
|
|
109
|
+
def __init__(self, start: int = 0) -> None:
|
|
110
|
+
self.value: int = start
|
|
111
|
+
|
|
112
|
+
def next(self) -> int:
|
|
113
|
+
self.value += 1
|
|
114
|
+
return self.value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def format_message(message: str) -> Markup:
|
|
118
|
+
if not message:
|
|
119
|
+
return Markup("")
|
|
120
|
+
|
|
121
|
+
escaped = escape(message).replace("\n", "<br>\n")
|
|
122
|
+
|
|
123
|
+
return Markup(
|
|
124
|
+
RX.sub(
|
|
125
|
+
r"<code>\1</code>",
|
|
126
|
+
str(escaped),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def lines_count(message: str) -> int:
|
|
132
|
+
if not message:
|
|
133
|
+
return 1
|
|
134
|
+
|
|
135
|
+
return int(1 + sum(len(line) / 50 for line in message.splitlines()))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def mult(a: int | str, b: int | str) -> int:
|
|
139
|
+
return int(a) * int(b)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def minus(a: int | str, b: int | str) -> int:
|
|
143
|
+
try:
|
|
144
|
+
return int(a) - int(b)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def gt(a: int | str, b: int | str) -> bool:
|
|
150
|
+
try:
|
|
151
|
+
return int(a) > int(b)
|
|
152
|
+
except (TypeError, ValueError):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def is_fuzzy(message: object) -> bool:
|
|
157
|
+
return bool(message and hasattr(message, "flags") and "fuzzy" in message.flags)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def linebreaksbr(value: object) -> Markup:
|
|
161
|
+
text = str(value)
|
|
162
|
+
return Markup(text.replace("\n", "<br>\n"))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def linebreaks(value: object) -> Markup:
|
|
166
|
+
text = str(value)
|
|
167
|
+
|
|
168
|
+
return Markup(
|
|
169
|
+
"".join(f"<p>{paragraph}</p>" for paragraph in text.splitlines() if paragraph)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def tojson(value: object) -> Markup:
|
|
174
|
+
|
|
175
|
+
def default(obj: object) -> object:
|
|
176
|
+
|
|
177
|
+
if isinstance(obj, Undefined):
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
if isinstance(obj, RosettaSettingsView):
|
|
181
|
+
return {key: getattr(obj, key) for key in obj._ALIASES}
|
|
182
|
+
|
|
183
|
+
return str(obj)
|
|
184
|
+
|
|
185
|
+
return Markup(
|
|
186
|
+
json.dumps(
|
|
187
|
+
value,
|
|
188
|
+
ensure_ascii=False,
|
|
189
|
+
default=default,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def register_jinja_filters(
|
|
195
|
+
env: Environment,
|
|
196
|
+
) -> Environment:
|
|
197
|
+
env.filters["can_translate"] = can_translate
|
|
198
|
+
env.filters["format_message"] = format_message
|
|
199
|
+
env.filters["lines_count"] = lines_count
|
|
200
|
+
env.filters["mult"] = mult
|
|
201
|
+
env.filters["minus"] = minus
|
|
202
|
+
env.filters["gt"] = gt
|
|
203
|
+
env.filters["is_fuzzy"] = is_fuzzy
|
|
204
|
+
env.filters["linebreaksbr"] = linebreaksbr
|
|
205
|
+
env.filters["linebreaks"] = linebreaks
|
|
206
|
+
env.filters["tojson"] = tojson
|
|
207
|
+
env.filters["increment"] = lambda counter: counter.next()
|
|
208
|
+
env.filters["add"] = lambda a, b: int(a) + int(b)
|
|
209
|
+
env.filters["length"] = len
|
|
210
|
+
env.filters["title"] = lambda value: str(value).title()
|
|
211
|
+
return env
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@lru_cache
|
|
215
|
+
def get_jinja_environment() -> Environment:
|
|
216
|
+
|
|
217
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
218
|
+
|
|
219
|
+
env = Environment(
|
|
220
|
+
loader=FileSystemLoader(str(templates_dir)),
|
|
221
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
register_jinja_filters(env)
|
|
225
|
+
|
|
226
|
+
return env
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def render_template(
|
|
230
|
+
template_name: str,
|
|
231
|
+
context: dict[str, Any],
|
|
232
|
+
*,
|
|
233
|
+
url_for: Any,
|
|
234
|
+
static: Any,
|
|
235
|
+
request: Any,
|
|
236
|
+
) -> str:
|
|
237
|
+
|
|
238
|
+
settings = get_settings()
|
|
239
|
+
|
|
240
|
+
languages: tuple[tuple[str, str], ...] = settings.languages or ()
|
|
241
|
+
|
|
242
|
+
default_language = languages[0][0] if languages else "en"
|
|
243
|
+
|
|
244
|
+
language = request.cookies.get("rosetta_language") or default_language
|
|
245
|
+
env = get_jinja_environment()
|
|
246
|
+
template = env.get_template(template_name)
|
|
247
|
+
render_context = {
|
|
248
|
+
**context,
|
|
249
|
+
"url": url_for,
|
|
250
|
+
"static": static,
|
|
251
|
+
"request": request,
|
|
252
|
+
"_": gettext_lazy(language),
|
|
253
|
+
"current_language": language,
|
|
254
|
+
"rosetta_languages": languages,
|
|
255
|
+
}
|
|
256
|
+
return template.render(render_context)
|
|
Binary file
|