- Add database fields for language preferences: - Vendor: dashboard_language, storefront_language, storefront_languages - User: preferred_language - Customer: preferred_language - Add language middleware for request-level language detection: - Cookie-based persistence - Browser Accept-Language fallback - Vendor storefront language constraints - Add language API endpoints (/api/v1/language/*): - POST /set - Set language preference - GET /current - Get current language info - GET /list - List available languages - DELETE /clear - Clear preference - Add i18n utilities (app/utils/i18n.py): - JSON-based translation loading - Jinja2 template integration - Language resolution helpers - Add reusable language selector macros for templates - Add languageSelector() Alpine.js component - Add translation files (en, fr, de, lb) in static/locales/ - Add architecture rules documentation for language implementation - Update marketplace-product-detail.js to use native language names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
187 lines
6.2 KiB
Python
187 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
|