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