Files
orion/middleware/language.py
Samir Boulahtit b769f5a047 refactor: centralize frontend detection with FrontendDetector
Major architecture change to unify frontend detection:

## Problem Solved
- Eliminated code duplication across 3 middleware files
- Fixed incomplete path detection (now detects /api/v1/admin/*)
- Unified on FrontendType enum (deprecates RequestContext)
- Added request.state.frontend_type for all requests

## New Components
- app/core/frontend_detector.py: Centralized FrontendDetector class
- middleware/frontend_type.py: FrontendTypeMiddleware (replaces ContextMiddleware)
- docs/architecture/frontend-detection.md: Complete architecture documentation

## Changes
- main.py: Use FrontendTypeMiddleware instead of ContextMiddleware
- middleware/context.py: Deprecated (kept for backwards compatibility)
- middleware/platform_context.py: Use FrontendDetector.is_admin()
- middleware/vendor_context.py: Use FrontendDetector.is_admin()
- middleware/language.py: Use FrontendType instead of context_value
- app/exceptions/handler.py: Use FrontendType.STOREFRONT
- app/exceptions/error_renderer.py: Use FrontendType
- Customer routes: Cookie path changed from /shop to /storefront

## Documentation
- docs/architecture/frontend-detection.md: New comprehensive docs
- docs/architecture/middleware.md: Updated for new system
- docs/architecture/request-flow.md: Updated for FrontendType
- docs/backend/middleware-reference.md: Updated API reference

## Tests
- tests/unit/core/test_frontend_detector.py: 37 new tests
- tests/unit/middleware/test_frontend_type.py: 11 new tests
- tests/unit/middleware/test_context.py: Updated for compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:15:19 +01:00

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
- 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.modules.enums import FrontendType
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 frontend type from FrontendTypeMiddleware
frontend_type = getattr(request.state, "frontend_type", 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 frontend type
if frontend_type == FrontendType.ADMIN:
# Admin dashboard: English only (for now)
# TODO: Implement admin language support later
language = "en"
elif frontend_type == FrontendType.VENDOR:
# 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 frontend_type == FrontendType.STOREFRONT:
# 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 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