Two issues caused the admin sidebar to show a mix of French and English:
1. Only 3 of 14 modules had "menu" translations in their locale files.
When a key was missing, _translate_label() fell back to English Title
Case from the key name — mixing with French from modules that had
translations. Added menu sections to all 4 languages (en, fr, de, lb)
across 13 modules.
2. The language middleware hardcoded admin to "en" ignoring user preference,
while the menu API fell back to DEFAULT_LANGUAGE ("fr") when
preferred_language was NULL. Fixed middleware to respect user's
preferred_language and menu API to use middleware-resolved language
as fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
6.4 KiB
Python
189 lines
6.4 KiB
Python
# middleware/language.py
|
|
"""
|
|
Language detection middleware for multi-language support.
|
|
|
|
This middleware detects the appropriate language for each request based on:
|
|
- User/Customer preferences (from JWT token)
|
|
- Session/cookie language
|
|
- Store settings
|
|
- Browser Accept-Language header
|
|
- System default
|
|
|
|
The resolved language is stored in request.state.language for use in templates.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
|
|
from app.modules.enums import FrontendType
|
|
from app.utils.i18n import (
|
|
DEFAULT_LANGUAGE,
|
|
SUPPORTED_LANGUAGES,
|
|
parse_accept_language,
|
|
resolve_storefront_language,
|
|
resolve_store_dashboard_language,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Cookie name for language preference
|
|
LANGUAGE_COOKIE_NAME = "lang"
|
|
|
|
|
|
class LanguageMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware to detect and set the request language.
|
|
|
|
Sets request.state.language based on context:
|
|
- Admin: Always English (for now)
|
|
- Store dashboard: User preference → Store dashboard_language → default
|
|
- Storefront: Customer preference → Cookie → Store storefront_language → browser → default
|
|
- API: Accept-Language header → default
|
|
"""
|
|
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
"""Process the request and set language."""
|
|
# Get frontend type from FrontendTypeMiddleware
|
|
frontend_type = getattr(request.state, "frontend_type", None)
|
|
|
|
# Get store from previous middleware (if available)
|
|
store = getattr(request.state, "store", None)
|
|
|
|
# Get language from cookie
|
|
cookie_language = request.cookies.get(LANGUAGE_COOKIE_NAME)
|
|
|
|
# Get browser language from Accept-Language header
|
|
accept_language = request.headers.get("accept-language")
|
|
browser_language = parse_accept_language(accept_language)
|
|
|
|
# Resolve language based on frontend type
|
|
if frontend_type == FrontendType.ADMIN:
|
|
# Admin dashboard: respect user's preferred language
|
|
user_preferred = self._get_user_language_from_token(request)
|
|
language = user_preferred or "en"
|
|
|
|
elif frontend_type == FrontendType.STORE:
|
|
# Store dashboard
|
|
user_preferred = self._get_user_language_from_token(request)
|
|
store_dashboard = store.dashboard_language if store else None
|
|
|
|
language = resolve_store_dashboard_language(
|
|
user_preferred=user_preferred,
|
|
store_dashboard=store_dashboard,
|
|
)
|
|
|
|
elif frontend_type == FrontendType.STOREFRONT:
|
|
# Storefront
|
|
customer_preferred = self._get_customer_language_from_token(request)
|
|
store_storefront = store.storefront_language if store else None
|
|
enabled_languages = store.storefront_languages if store else None
|
|
|
|
language = resolve_storefront_language(
|
|
customer_preferred=customer_preferred,
|
|
session_language=cookie_language,
|
|
store_storefront=store_storefront,
|
|
browser_language=browser_language,
|
|
enabled_languages=enabled_languages,
|
|
)
|
|
|
|
elif frontend_type == FrontendType.PLATFORM:
|
|
# Platform marketing pages: Use cookie, browser, or default
|
|
language = cookie_language or browser_language or DEFAULT_LANGUAGE
|
|
|
|
else:
|
|
# Fallback (API or unknown): Use Accept-Language or cookie
|
|
language = cookie_language or browser_language or DEFAULT_LANGUAGE
|
|
|
|
# Validate language is supported
|
|
if language not in SUPPORTED_LANGUAGES:
|
|
language = DEFAULT_LANGUAGE
|
|
|
|
# Store language in request state
|
|
request.state.language = language
|
|
|
|
# Also store related info for templates
|
|
request.state.language_info = {
|
|
"code": language,
|
|
"cookie": cookie_language,
|
|
"browser": browser_language,
|
|
"frontend_type": frontend_type.value if frontend_type else None,
|
|
}
|
|
|
|
# Log language detection for debugging
|
|
frontend_value = frontend_type.value if frontend_type else "unknown"
|
|
logger.debug(
|
|
f"Language detected: {language} "
|
|
f"(frontend={frontend_value}, cookie={cookie_language}, browser={browser_language})"
|
|
)
|
|
|
|
# Process request
|
|
response = await call_next(request)
|
|
|
|
return response
|
|
|
|
def _get_user_language_from_token(self, request: Request) -> str | None:
|
|
"""
|
|
Extract user's preferred_language from JWT token.
|
|
|
|
This requires the auth middleware to have run first and stored
|
|
user info in request.state.
|
|
"""
|
|
# Check if user info is in request state (set by auth dependency)
|
|
current_user = getattr(request.state, "current_user", None)
|
|
if current_user and hasattr(current_user, "preferred_language"):
|
|
return current_user.preferred_language
|
|
|
|
return None
|
|
|
|
def _get_customer_language_from_token(self, request: Request) -> str | None:
|
|
"""
|
|
Extract customer's preferred_language from JWT token.
|
|
|
|
This requires the shop auth middleware to have run first.
|
|
"""
|
|
# Check if customer info is in request state
|
|
current_customer = getattr(request.state, "current_customer", None)
|
|
if current_customer and hasattr(current_customer, "preferred_language"):
|
|
return current_customer.preferred_language
|
|
|
|
return None
|
|
|
|
|
|
def set_language_cookie(response: Response, language: str) -> Response:
|
|
"""
|
|
Helper function to set the language cookie on a response.
|
|
|
|
Args:
|
|
response: Response object to modify
|
|
language: Language code to set
|
|
|
|
Returns:
|
|
Modified response with language cookie
|
|
"""
|
|
if language in SUPPORTED_LANGUAGES:
|
|
response.set_cookie(
|
|
key=LANGUAGE_COOKIE_NAME,
|
|
value=language,
|
|
max_age=60 * 60 * 24 * 365, # 1 year
|
|
httponly=False, # Accessible to JavaScript
|
|
samesite="lax",
|
|
)
|
|
return response
|
|
|
|
|
|
def delete_language_cookie(response: Response) -> Response:
|
|
"""
|
|
Helper function to delete the language cookie.
|
|
|
|
Args:
|
|
response: Response object to modify
|
|
|
|
Returns:
|
|
Modified response with cookie deleted
|
|
"""
|
|
response.delete_cookie(key=LANGUAGE_COOKIE_NAME)
|
|
return response
|