Features: 1) Add language switching functionality with URL-based language detection and session/cookie persistence;

Fixes: 1) Ensure safe next URL handling with host validation;

Extra: 1) Introduce new i18n module with language normalization and translation support; 2) Add route for language setting endpoint.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-16 01:23:09 +03:00
parent efa21cf9c0
commit 555666d6fe
2 changed files with 81 additions and 0 deletions

75
evibes/i18n.py Normal file
View file

@ -0,0 +1,75 @@
from __future__ import annotations
from contextlib import suppress
from urllib.parse import urlparse
from django.conf import settings
from django.http import HttpRequest, HttpResponseRedirect
from django.urls import translate_url
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import activate
from django.views.decorators.csrf import csrf_exempt
def _normalize_language_code(lang: str | None) -> str:
if not lang:
return settings.LANGUAGE_CODE
lang = lang.replace("_", "-").lower()
overrides = getattr(settings, "LANGUAGE_URL_OVERRIDES", {}) or {}
if lang in overrides:
lang = overrides[lang].lower()
supported = {code.lower() for code, _ in getattr(settings, "LANGUAGES", [])}
if lang not in supported:
primary = lang.split("-")[0]
if primary in overrides and overrides[primary].lower() in supported:
return overrides[primary].lower()
return settings.LANGUAGE_CODE
return lang
def _safe_next_url(request: HttpRequest) -> str:
next_url = request.POST.get("next") or request.GET.get("next") or request.META.get("HTTP_REFERER") or "/"
if not url_has_allowed_host_and_scheme(
url=next_url,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
):
return "/"
return next_url
LANGUAGE_SESSION_KEY = "_language"
@csrf_exempt
def set_language(request: HttpRequest):
language = request.POST.get("language") or request.GET.get("language")
normalized = _normalize_language_code(language)
response = HttpResponseRedirect("/")
if hasattr(request, "session"):
request.session[LANGUAGE_SESSION_KEY] = normalized
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, normalized)
activate(normalized)
next_url = _safe_next_url(request)
if translate_url is not None:
with suppress(Exception):
next_url = translate_url(next_url, normalized)
else:
parsed = urlparse(next_url)
parts = [p for p in parsed.path.split("/") if p]
supported = {code.lower() for code, _ in getattr(settings, "LANGUAGES", [])}
if parts and parts[0].lower() in supported:
parts[0] = normalized
path = "/" + "/".join(parts) + "/" if parsed.path.endswith("/") else "/" + "/".join(parts)
next_url = parsed._replace(path=path).geturl()
response["Location"] = next_url
return response

View file

@ -12,6 +12,7 @@ from engine.core.views import (
favicon_view, favicon_view,
index, index,
) )
from evibes.i18n import set_language
urlpatterns = [ urlpatterns = [
### COMMON URLS ### ### COMMON URLS ###
@ -35,6 +36,11 @@ urlpatterns = [
"summernote/", "summernote/",
include("django_summernote.urls"), include("django_summernote.urls"),
), ),
path(
r"i18n/setlang/",
set_language,
name="set_language",
),
path( path(
r"i18n/", r"i18n/",
include("django.conf.urls.i18n"), include("django.conf.urls.i18n"),