Created
September 20, 2022 15:35
-
-
Save u1735067/ad36115e455a43dc6125930c28c91daa to your computer and use it in GitHub Desktop.
Django ASGI Locale middleware `LocaleAsgiMiddleware` (not well-tested)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
ASGI config for MyApp project. | |
It exposes the ASGI callable as a module-level variable named ``application``. | |
For more information on this file, see | |
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ | |
""" | |
import os | |
from django.core.asgi import get_asgi_application | |
from channels.routing import ProtocolTypeRouter, URLRouter | |
from channels.auth import AuthMiddlewareStack | |
from channels.security.websocket import AllowedHostsOriginValidator | |
from myapp.core import urls | |
from myapp.core.middleware import LocaleAsgiMiddleware | |
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") | |
def AsgiWsMiddlewares(inner): | |
return ( | |
AllowedHostsOriginValidator( | |
AuthMiddlewareStack( | |
LocaleAsgiMiddleware( | |
inner | |
) | |
) | |
) | |
) | |
application = ProtocolTypeRouter( | |
{ | |
"http": get_asgi_application(), | |
"websocket": AsgiWsMiddlewares(URLRouter(urls.websocket_urlpatterns)), | |
} | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import functools | |
from django.conf import settings | |
from django.urls import LocalePrefixPattern | |
from django.utils import translation | |
from django.utils.translation import trans_real | |
from channels.middleware import BaseMiddleware | |
from channels.routing import URLRouter | |
class LocaleAsgiMiddleware(BaseMiddleware): | |
""" | |
Set translation language from path, cookie or http headers, save language as | |
scope["lang"] and add scope["path_info"] as a bonus. | |
Requires the CookieMiddleware. | |
""" | |
def __init__(self, inner): | |
""" | |
Init the middleware by finding the URLRouter | |
""" | |
self.inner = inner | |
try: | |
inner_urlrouter = inner | |
while not isinstance(inner_urlrouter, URLRouter): | |
inner_urlrouter = inner.inner | |
self.urlrouter = inner_urlrouter | |
except AttributeError: | |
raise ValueError('Unable to find URLRouter object in inner application') | |
async def __call__(self, scope, receive, send): | |
""" | |
Customize the scope by assing lang and path_info | |
""" | |
# Copy scope to stop changes going upstream | |
scope = dict(scope) | |
# Apply changes | |
scope['path_info'] = self.get_path_info(scope) | |
scope['lang'] = self.set_lang(scope) | |
# Run the inner application along with the scope | |
return await self.inner(scope, receive, send) | |
# https://github.dev/django/django/blob/stable/4.0.x/django/core/handlers/asgi.py#L42 | |
@staticmethod | |
def get_path_info(scope): | |
""" | |
Return path_info constructed from scope | |
""" | |
script_name = scope.get("root_path", "") | |
if script_name and scope["path"].startswith(script_name): | |
return scope["path"][len(script_name):] | |
else: | |
return scope["path"] | |
# https://github.dev/django/django/blob/stable/4.0.x/django/middleware/locale.py#L19 | |
def set_lang(self, scope): | |
""" | |
Parse the scope and decide what translation object to install in the | |
current asgiref context. This allows pages to be dynamically translated | |
to the language the user desires (if the language is available). | |
""" | |
( | |
i18n_patterns_used, | |
prefixed_default_language, | |
) = self._is_language_prefix_patterns_used(self.urlrouter) | |
language = self._get_language_from_request( | |
scope, check_path=i18n_patterns_used | |
) | |
language_from_path = translation.get_language_from_path(scope['path']) | |
if ( | |
not language_from_path | |
and i18n_patterns_used | |
and not prefixed_default_language | |
): | |
language = settings.LANGUAGE_CODE | |
translation.activate(language) | |
return translation.get_language() | |
# https://github.dev/django/django/blob/stable/4.0.x/django/conf/urls/i18n.py#L24 | |
@staticmethod | |
@functools.lru_cache(maxsize=None) | |
def _is_language_prefix_patterns_used(urlrouter): | |
""" | |
Return a tuple of two booleans: ( | |
`True` if i18n_patterns() (LocalePrefixPattern) is used in the URLconf, | |
`True` if the default language should be prefixed | |
) | |
""" | |
for route in urlrouter.routes: | |
if isinstance(route, LocalePrefixPattern): | |
return True, route.prefix_default_language | |
return False, False | |
# https://github.dev/django/django/blob/stable/4.0.x/django/utils/translation/trans_real.py#L540 | |
@staticmethod | |
def _get_language_from_request(scope, check_path=False): | |
""" | |
Analyze the scope to find what language the user wants the system to | |
show. Only languages listed in settings.LANGUAGES are taken into account. | |
If the user requests a sublanguage where we have a main language, we send | |
out the main language. | |
If check_path is True, the URL path prefix will be checked for a language | |
code, otherwise this is skipped for backwards compatibility. | |
""" | |
# Reproduce the translation.* behaviour that returns trans_null value | |
if not settings.USE_I18N: | |
return settings.LANGUAGE_CODE | |
if check_path: | |
lang_code = translation.get_language_from_path(scope['path']) | |
if lang_code is not None: | |
return lang_code | |
if 'cookies' not in scope: | |
raise ValueError( | |
"No cookies in scope - LocaleMiddleware needs to run " | |
"inside of CookieMiddleware." | |
) | |
lang_code = scope['cookies'].get(settings.LANGUAGE_COOKIE_NAME) | |
if ( | |
lang_code is not None | |
and lang_code in trans_real.get_languages() | |
and translation.check_for_language(lang_code) | |
): | |
return lang_code | |
try: | |
return translation.get_supported_language_variant(lang_code) | |
except LookupError: | |
pass | |
# # Go through headers to find the cookie one | |
# for name, value in scope.get("headers", []): | |
# if name == b"accept-language": | |
# accept = value.decode("latin1") | |
# break | |
# else: | |
# accept = "" | |
accept = { | |
k.lower(): v for k, v in scope['headers'] | |
}.get("accept-language", b"").decode('latin1') | |
for accept_lang, unused in trans_real.parse_accept_lang_header(accept): | |
if accept_lang == "*": | |
break | |
if not trans_real.language_code_re.search(accept_lang): | |
continue | |
try: | |
return translation.get_supported_language_variant(accept_lang) | |
except LookupError: | |
continue | |
try: | |
return translation.get_supported_language_variant(settings.LANGUAGE_CODE) | |
except LookupError: | |
return settings.LANGUAGE_CODE |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment