108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
# /opt/docker/dev/service_finder/backend/app/core/i18n_middleware.py
|
|
"""
|
|
FastAPI middleware for automatic locale/language detection and context management.
|
|
Implements the priority order:
|
|
1. ?lang= query parameter
|
|
2. Accept-Language HTTP header
|
|
3. User profile language (if authenticated)
|
|
4. Default "hu" (Hungarian)
|
|
"""
|
|
import logging
|
|
from typing import Optional
|
|
from fastapi import Request, HTTPException
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.responses import Response
|
|
|
|
from app.core.context import set_current_locale, get_current_locale
|
|
from app.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class I18nMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware for automatic locale detection and context management.
|
|
Sets the locale in contextvars for the duration of the request.
|
|
"""
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
# Determine locale based on priority
|
|
locale = self._determine_locale(request)
|
|
|
|
# Set locale in context
|
|
set_current_locale(locale)
|
|
|
|
# Add locale to request state for debugging/logging
|
|
request.state.locale = locale
|
|
|
|
# Process request
|
|
response = await call_next(request)
|
|
|
|
# Optionally add locale header to response
|
|
response.headers["X-Content-Language"] = locale
|
|
|
|
return response
|
|
|
|
def _determine_locale(self, request: Request) -> str:
|
|
"""
|
|
Determine the locale based on the priority order.
|
|
Returns a valid locale code (e.g., "hu", "en", "de").
|
|
"""
|
|
# 1. Query parameter: ?lang=
|
|
query_lang = request.query_params.get("lang")
|
|
if query_lang and self._is_valid_locale(query_lang):
|
|
logger.debug(f"Locale from query param: {query_lang}")
|
|
return query_lang
|
|
|
|
# 2. Accept-Language HTTP header
|
|
accept_language = request.headers.get("accept-language")
|
|
if accept_language:
|
|
header_lang = self._parse_accept_language(accept_language)
|
|
if header_lang and self._is_valid_locale(header_lang):
|
|
logger.debug(f"Locale from Accept-Language header: {header_lang}")
|
|
return header_lang
|
|
|
|
# 3. User profile language (if authenticated)
|
|
# Note: This requires the user to be authenticated, which may not be available
|
|
# in middleware before authentication. We'll handle this in endpoint dependencies.
|
|
# For now, we'll skip this step in middleware and let endpoints handle it.
|
|
|
|
# 4. Default locale
|
|
default_locale = getattr(settings, "DEFAULT_LOCALE", "hu")
|
|
logger.debug(f"Using default locale: {default_locale}")
|
|
return default_locale
|
|
|
|
def _parse_accept_language(self, accept_language: str) -> Optional[str]:
|
|
"""
|
|
Parse Accept-Language header and return the highest priority locale.
|
|
Example: "en-US,en;q=0.9,hu;q=0.8" -> "en"
|
|
"""
|
|
try:
|
|
# Split by comma and process each language range
|
|
languages = accept_language.split(',')
|
|
for lang_range in languages:
|
|
# Remove quality factor if present
|
|
lang = lang_range.split(';')[0].strip()
|
|
# Extract primary language subtag (e.g., "en" from "en-US")
|
|
if '-' in lang:
|
|
lang = lang.split('-')[0]
|
|
if self._is_valid_locale(lang):
|
|
return lang
|
|
except Exception as e:
|
|
logger.debug(f"Error parsing Accept-Language header: {e}")
|
|
|
|
return None
|
|
|
|
def _is_valid_locale(self, locale: str) -> bool:
|
|
"""
|
|
Check if a locale code is valid/supported.
|
|
For now, we accept any 2-5 character locale code.
|
|
In production, you might want to check against a list of supported locales.
|
|
"""
|
|
if not locale or len(locale) < 2 or len(locale) > 5:
|
|
return False
|
|
|
|
# Basic validation: only letters and hyphens
|
|
return locale.replace('-', '').isalpha()
|
|
|
|
# Create middleware instance (commented out to avoid instantiation error during import)
|
|
# i18n_middleware = I18nMiddleware() |