From a7abde68a2e5b882d937cc4b4f373cea125e5a41 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Thu, 28 Aug 2025 12:02:18 +0200 Subject: [PATCH] Language middleware: user and Accept-Language _only_ (as a matter of taste: I prefer to keep this as simple as possible) See #161 --- bugsink/middleware.py | 47 +++++++++++++++++++++---------------- bugsink/settings/default.py | 4 +++- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/bugsink/middleware.py b/bugsink/middleware.py index 0680c9c..f19c966 100644 --- a/bugsink/middleware.py +++ b/bugsink/middleware.py @@ -5,6 +5,10 @@ from django.contrib.auth.decorators import login_required from django.db import connection from django.conf import settings from django.core.exceptions import SuspiciousOperation +from django.utils.translation import get_supported_language_variant +from django.utils.translation.trans_real import parse_accept_lang_header +from django.utils import translation + performance_logger = logging.getLogger("bugsink.performance.views") @@ -130,30 +134,33 @@ class SetRemoteAddrMiddleware: return self.get_response(request) +def language_from_accept_language(request): + """ + Pick a language using ONLY the Accept-Language header. Ignores URL prefixes, session, and cookies. I prefer to have + as little "magic" in the language selection as possible, and I _know_ we don't do anything with paths, so I'd rather + not have such code invoked at all (at the cost of reimplementing some of Django's logic here). + """ + header = request.META.get("HTTP_ACCEPT_LANGUAGE", "") + for lang_code, _q in parse_accept_lang_header(header): + try: + # strict=False lets country variants match (e.g. 'es-CO' for 'es') + return get_supported_language_variant(lang_code, strict=False) + except LookupError: + continue + return settings.LANGUAGE_CODE + + class UserLanguageMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): + if request.user.is_authenticated and request.user.language != "auto": + normalized_explicit_lang = get_supported_language_variant(request.user.language, strict=False) + translation.activate(normalized_explicit_lang) + else: + auto_lang = language_from_accept_language(request) + translation.activate(auto_lang) + response = self.get_response(request) - - if (request.user.is_authenticated and - hasattr(request.user, 'language')): - - user_language = request.user.language - current_cookie = request.COOKIES.get('django_language') - - if user_language == 'auto': - if current_cookie is not None: - response.delete_cookie('django_language') - else: - if current_cookie != user_language: - response.set_cookie( - 'django_language', - user_language, - max_age=365 * 24 * 60 * 60, - httponly=False, - samesite='Lax' - ) - return response diff --git a/bugsink/settings/default.py b/bugsink/settings/default.py index 6fb76b7..ffd3b52 100644 --- a/bugsink/settings/default.py +++ b/bugsink/settings/default.py @@ -97,12 +97,14 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'verbose_csrf_middleware.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'bugsink.middleware.LoginRequiredMiddleware', + + # note on ordering: we need request.user, so after AuthenticationMiddleware; and we're not tied to "before + # CommonMiddleware" as django.middleware.locale.LocaleMiddleware is, because we don't do path-related stuff. 'bugsink.middleware.UserLanguageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',