Files
blog.pyaqa.ru/app/presentation/web/locale.py
Sergey Vanyushkin d32ad29abc
Some checks failed
ci/woodpecker/pr/pipeline Pipeline failed
feat(i18n): add browser-language localization with Jinja2 _() and locale middleware
Add i18n support to the blog web UI with 4 languages (en/ru/fr/de),
80 translation keys, automatic Accept-Language detection, persistent
locale cookie, and a language switcher dropdown in the header.

- Infrastructure: TranslationService, translation dicts, convenience _()
- Presentation: locale middleware, /web/lang/{locale} switcher route
- Templates: all 9 templates use {{ _(key, current_locale) }}
- Tests: 26 tests across TranslationService, locale detection helpers
- Docs: TEST_MODEL.md and FEATURE_INFRASTRUCTURE.md updated with TC-UNIT-811-821
2026-05-10 16:22:06 +03:00

73 lines
2.0 KiB
Python

"""Locale detection and management for i18n support.
This module provides locale detection from Accept-Language headers and cookies,
following the same middleware pattern as the flash message system.
"""
from fastapi import Request
from app.infrastructure.i18n.translator import DEFAULT_LOCALE, SUPPORTED_LOCALES
LOCALE_COOKIE_NAME = "locale"
SUPPORTED_LOCALES_SET: frozenset[str] = SUPPORTED_LOCALES
def _parse_accept_language(header: str) -> list[str]:
"""Parse Accept-Language header into ordered list of locale codes.
Args:
header: Raw Accept-Language header value.
Returns:
List of locale codes in preference order, with region subtags removed.
"""
if not header:
return []
locales: list[str] = []
for part in header.split(","):
part = part.strip()
if not part:
continue
locale = part.split(";")[0].strip().split("-")[0]
if locale:
locales.append(locale)
return locales
def _get_best_locale(request: Request) -> str:
"""Detect the best locale for the current request.
Priority order: cookie → Accept-Language header → default.
Args:
request: FastAPI request object.
Returns:
Best matching locale code, defaulting to ``en``.
"""
cookie_locale = request.cookies.get(LOCALE_COOKIE_NAME)
if cookie_locale and cookie_locale in SUPPORTED_LOCALES_SET:
return cookie_locale
accept_language = request.headers.get("accept-language", "")
for lang in _parse_accept_language(accept_language):
if lang in SUPPORTED_LOCALES_SET:
return lang
return DEFAULT_LOCALE
async def setup_locale_manager(request: Request) -> None:
"""Set the detected locale on request state.
Called early in the request lifecycle so that route handlers and
template rendering can access the current locale via
``request.state.locale``.
Args:
request: FastAPI request object.
"""
if not hasattr(request.state, "locale"):
request.state.locale = _get_best_locale(request)