Files
orion/middleware/language.py
Samir Boulahtit 6c78827c7f
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 46m27s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat: add language switching to admin and merchant frontends
- Add cookie to ADMIN resolution chain (cookie → user_pref → "en")
- Add explicit MERCHANT resolution (cookie → user_pref → "fr")
- Add language selector dropdown to admin and merchant headers
- Add languageSelector() function to merchant init-alpine.js
- Add flag-icons CSS and i18n.js setup to merchant base template
- Add compact flag-based language selector to both login pages
- Make lang attribute dynamic on all base and login templates
- Pass current_language to login route template context
- Update architecture doc with ADMIN/MERCHANT resolution priorities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:26:57 +01:00

197 lines
6.9 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_store_dashboard_language,
resolve_storefront_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: Cookie → User preference → English
- Merchant: Cookie → User preference → Platform default
- Store dashboard: Cookie → User preference → Store dashboard_language → default
- Storefront: Customer preference → Cookie → Store storefront_language → browser → default
- Platform: Cookie → Browser → Platform 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: cookie → user preference → English default
user_preferred = self._get_user_language_from_token(request)
language = cookie_language or 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,
cookie_language=cookie_language,
)
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.MERCHANT:
# Merchant portal: cookie → user preference → platform default
user_preferred = self._get_user_language_from_token(request)
language = cookie_language or user_preferred or DEFAULT_LANGUAGE
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",
path="/",
)
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, path="/")
return response