Files
service-finder/backend/app/core/i18n_middleware.py
2026-06-04 07:26:22 +00:00

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()