Files
orion/middleware/language.py
Samir Boulahtit 9920430b9e fix: correct tojson|safe usage in templates and update validator
- Remove |safe from |tojson in HTML attributes (x-data) - quotes must
  become " for browsers to parse correctly
- Update LANG-002 and LANG-003 architecture rules to document correct
  |tojson usage patterns:
  - HTML attributes: |tojson (no |safe)
  - Script blocks: |tojson|safe
- Fix validator to warn when |tojson|safe is used in x-data (breaks
  HTML attribute parsing)
- Improve code quality across services, APIs, and tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 22:59:51 +01:00

188 lines
6.2 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
- Vendor 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.utils.i18n import (
DEFAULT_LANGUAGE,
SUPPORTED_LANGUAGES,
parse_accept_language,
resolve_storefront_language,
resolve_vendor_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)
- Vendor dashboard: User preference → Vendor dashboard_language → default
- Storefront: Customer preference → Cookie → Vendor 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 context type from previous middleware
context_type = getattr(request.state, "context_type", None)
context_value = context_type.value if context_type else None
# Get vendor from previous middleware (if available)
vendor = getattr(request.state, "vendor", 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 context
if context_value == "admin":
# Admin dashboard: English only (for now)
# TODO: Implement admin language support later
language = "en"
elif context_value == "vendor_dashboard":
# Vendor dashboard
user_preferred = self._get_user_language_from_token(request)
vendor_dashboard = vendor.dashboard_language if vendor else None
language = resolve_vendor_dashboard_language(
user_preferred=user_preferred,
vendor_dashboard=vendor_dashboard,
)
elif context_value == "shop":
# Storefront
customer_preferred = self._get_customer_language_from_token(request)
vendor_storefront = vendor.storefront_language if vendor else None
enabled_languages = vendor.storefront_languages if vendor else None
language = resolve_storefront_language(
customer_preferred=customer_preferred,
session_language=cookie_language,
vendor_storefront=vendor_storefront,
browser_language=browser_language,
enabled_languages=enabled_languages,
)
elif context_value == "api":
# API requests: Use Accept-Language or cookie
language = cookie_language or browser_language or DEFAULT_LANGUAGE
else:
# Fallback: Use cookie, browser, or default
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,
"context": context_value,
}
# Log language detection for debugging
logger.debug(
f"Language detected: {language} "
f"(context={context_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