feat: add multi-language (i18n) support for vendor dashboard and storefront

- 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>
This commit is contained in:
2025-12-13 22:36:09 +01:00
parent d21cd366dc
commit d2b05441fc
30 changed files with 4615 additions and 33 deletions

186
middleware/language.py Normal file
View File

@@ -0,0 +1,186 @@
# 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