Some checks failed
ci/woodpecker/pr/pipeline Pipeline failed
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
73 lines
2.0 KiB
Python
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)
|