Skip to content

Instantly share code, notes, and snippets.

@u1735067
Created September 20, 2022 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save u1735067/ad36115e455a43dc6125930c28c91daa to your computer and use it in GitHub Desktop.
Save u1735067/ad36115e455a43dc6125930c28c91daa to your computer and use it in GitHub Desktop.
Django ASGI Locale middleware `LocaleAsgiMiddleware` (not well-tested)
"""
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)),
}
)
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