# Language & Internationalization (i18n) Architecture This document defines **strict rules** for implementing language support across the Orion platform. > **IMPORTANT:** These rules are mandatory. Violations will cause runtime errors, inconsistent UX, or security issues. --- ## Table of Contents 1. [Supported Languages](#supported-languages) 2. [Language Context Flow](#language-context-flow) 3. [Database Schema Rules](#database-schema-rules) 4. [Frontend Rules](#frontend-rules) 5. [API Rules](#api-rules) 6. [Template Rules](#template-rules) 7. [JavaScript Rules](#javascript-rules) 8. [Translation File Rules](#translation-file-rules) --- ## Supported Languages | Code | Language | Flag Code | Notes | |------|----------|-----------|-------| | `en` | English | `gb` | Fallback language | | `fr` | French | `fr` | Default for Luxembourg | | `de` | German | `de` | Second official language | | `lb` | Luxembourgish | `lu` | Native language | **Rule LANG-001: Only Use Supported Language Codes** ```python # ✅ GOOD: Use supported codes SUPPORTED_LANGUAGES = ["en", "fr", "de", "lb"] # ❌ BAD: Invalid codes language = "english" # ❌ Use "en" language = "french" # ❌ Use "fr" language = "lux" # ❌ Use "lb" ``` --- ## Language Context Flow ### Resolution Priority (per Frontend Type) Language resolution varies by frontend type. Each chain is evaluated top-to-bottom, first match wins: | Frontend | Priority (highest → lowest) | |----------|----------------------------| | **ADMIN** | Cookie (`lang`) → User `preferred_language` → `"en"` | | **MERCHANT** | Cookie (`lang`) → User `preferred_language` → `"fr"` | | **STORE** | Cookie (`lang`) → User `preferred_language` → Store `dashboard_language` → `"fr"` | | **STOREFRONT** | Customer `preferred_language` → Cookie (`lang`) → Store `storefront_language` → Browser `Accept-Language` → `"fr"` | | **PLATFORM** | Cookie (`lang`) → Browser `Accept-Language` → `"fr"` | The **cookie** (`lang`) is set by the language switcher UI via `POST /api/v1/platform/language/set` and represents the user's most recent explicit language choice. It takes priority over database preferences because it reflects an immediate UI action. ### Database Fields | Context | Language Source | Database Field | |---------|-----------------|----------------| | Admin Panel | User's `preferred_language` | `users.preferred_language` | | Merchant Portal | User's `preferred_language` | `users.preferred_language` | | Store Dashboard | Store's `dashboard_language` | `stores.dashboard_language` | | Customer Storefront | Store's `storefront_language` | `stores.storefront_language` | --- ## Database Schema Rules ### Rule DB-001: Store Language Fields Are Required **Stores MUST have these language columns with defaults:** ```python # ✅ GOOD: All language fields with defaults class Store(Base): default_language = Column(String(5), nullable=False, default="fr") dashboard_language = Column(String(5), nullable=False, default="fr") storefront_language = Column(String(5), nullable=False, default="fr") storefront_languages = Column(JSON, nullable=False, default=["fr", "de", "en"]) ``` ```python # ❌ BAD: Nullable language fields class Store(Base): default_language = Column(String(5), nullable=True) # ❌ Must have default ``` ### Rule DB-002: User/Customer Preferred Language Is Optional ```python # ✅ GOOD: Optional with fallback logic class User(Base): preferred_language = Column(String(5), nullable=True) # Falls back to store/platform default class Customer(Base): preferred_language = Column(String(5), nullable=True) # Falls back to storefront_language ``` ### Rule DB-003: Pydantic Schemas Must Handle Missing Language Fields ```python # ✅ GOOD: Optional in response with defaults class StoreResponse(BaseModel): default_language: str = "fr" dashboard_language: str = "fr" storefront_language: str = "fr" storefront_languages: list[str] = ["fr", "de", "en"] # ❌ BAD: Required without defaults (breaks backward compatibility) class StoreResponse(BaseModel): default_language: str # ❌ Will fail if DB doesn't have value ``` --- ## Frontend Rules ### Rule FE-001: NEVER Use Inline Complex Alpine.js Data for Language Selector **Move complex JavaScript objects to functions. Inline x-data with Jinja breaks JSON serialization.** ```html
``` ```html
``` ```javascript // ✅ GOOD: Define function in JavaScript file function languageSelector(currentLang, enabledLanguages) { return { isLangOpen: false, currentLang: currentLang || 'fr', languages: enabledLanguages || ['fr', 'de', 'en'], languageNames: { 'en': 'English', 'fr': 'Français', 'de': 'Deutsch', 'lb': 'Lëtzebuergesch' }, languageFlags: { 'en': 'gb', 'fr': 'fr', 'de': 'de', 'lb': 'lu' }, async setLanguage(lang) { // Implementation } }; } ``` ### Rule FE-002: Always Use tojson|safe for Python Lists in JavaScript ```html
``` ### Rule FE-003: Language Selector Must Be In Shared JavaScript **Language selector function MUST be defined in:** - `static/shop/js/shop-layout.js` for storefront - `static/store/js/init-alpine.js` for store dashboard ```javascript // ✅ GOOD: Reusable function with consistent implementation function languageSelector(currentLang, enabledLanguages) { return { isLangOpen: false, currentLang: currentLang || 'fr', languages: enabledLanguages || ['en', 'fr', 'de', 'lb'], languageNames: { 'en': 'English', 'fr': 'Français', 'de': 'Deutsch', 'lb': 'Lëtzebuergesch' }, languageFlags: { 'en': 'gb', 'fr': 'fr', 'de': 'de', 'lb': 'lu' }, async setLanguage(lang) { if (lang === this.currentLang) { this.isLangOpen = false; return; } try { const response = await fetch('/api/v1/platform/language/set', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ language: lang }) }); if (response.ok) { this.currentLang = lang; window.location.reload(); } } catch (error) { console.error('Failed to set language:', error); } this.isLangOpen = false; } }; } window.languageSelector = languageSelector; ``` ### Rule FE-004: Storefront Must Respect Store's Enabled Languages ```html {% set enabled_langs = store.storefront_languages if store and store.storefront_languages else ['fr', 'de', 'en'] %} {% if enabled_langs|length > 1 %}
{% endif %} ``` ```html
``` ### Rule FE-005: Store Dashboard Shows All Languages ```html
``` --- ## API Rules ### Rule API-001: Language Endpoint Must Set Cookie ```python # ✅ GOOD: Set both cookie and return JSON @router.post("/language/set") async def set_language(request: LanguageSetRequest, response: Response): if request.language not in SUPPORTED_LANGUAGES: raise HTTPException(status_code=400, detail="Unsupported language") response.set_cookie( key="lang", value=request.language, max_age=365 * 24 * 60 * 60, # 1 year httponly=False, # Accessible to JavaScript samesite="lax", path="/", # Must be site-wide ) return {"success": True, "language": request.language} ``` ### Rule API-002: Validate Language Codes ```python # ✅ GOOD: Strict validation SUPPORTED_LANGUAGES = {"en", "fr", "de", "lb"} class LanguageSetRequest(BaseModel): language: str = Field(..., pattern="^(en|fr|de|lb)$") # ❌ BAD: No validation class LanguageSetRequest(BaseModel): language: str # ❌ Accepts any string ``` --- ## Template Rules ### Rule TPL-001: Always Provide Language Default ```html {{ request.state.language|default("fr") }} {{ request.state.language }} ``` ### Rule TPL-002: Check Language Array Length Before Rendering Selector ```html {% if enabled_langs|length > 1 %} {% endif %} ``` ### Rule TPL-003: Use Consistent Flag Icon Classes ```html ``` --- ## JavaScript Rules ### Rule JS-001: Language Names Must Use Native Language ```javascript // ✅ GOOD: Native language names languageNames: { 'en': 'English', 'fr': 'Français', // ✅ Not "French" 'de': 'Deutsch', // ✅ Not "German" 'lb': 'Lëtzebuergesch' // ✅ Not "Luxembourgish" } // ❌ BAD: English names languageNames: { 'en': 'English', 'fr': 'French', // ❌ Should be "Français" 'de': 'German', // ❌ Should be "Deutsch" 'lb': 'Luxembourgish' // ❌ Should be "Lëtzebuergesch" } ``` ### Rule JS-002: Flag Codes Must Map Correctly ```javascript // ✅ GOOD: Correct flag mappings languageFlags: { 'en': 'gb', // ✅ Great Britain flag for English 'fr': 'fr', // ✅ France flag 'de': 'de', // ✅ Germany flag 'lb': 'lu' // ✅ Luxembourg flag } // ❌ BAD: Incorrect mappings languageFlags: { 'en': 'us', // ❌ US flag is incorrect for general English 'en': 'en', // ❌ 'en' is not a valid flag code 'lb': 'lb' // ❌ 'lb' is not a valid flag code } ``` ### Rule JS-003: Language API Must Use Correct Endpoint ```javascript // ✅ GOOD: Correct endpoint fetch('/api/v1/platform/language/set', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ language: lang }) }); // ❌ BAD: Wrong endpoint or method fetch('/api/v1/language/set', ...); // ❌ Missing /platform fetch('/api/v1/platform/language', ...); // ❌ Missing /set fetch('/api/v1/platform/language/set', { method: 'GET' }); // ❌ Should be POST ``` --- ## Translation File Rules ### Rule TRANS-001: All Translation Keys Must Exist in All Files ``` static/locales/ ├── en.json # Must have ALL keys ├── fr.json # Must have ALL keys ├── de.json # Must have ALL keys └── lb.json # Must have ALL keys ``` ### Rule TRANS-002: Translation Files Must Be Valid JSON ```json // ✅ GOOD: Valid JSON { "common": { "save": "Save", "cancel": "Cancel" } } // ❌ BAD: Trailing comma { "common": { "save": "Save", "cancel": "Cancel", // ❌ Trailing comma } } ``` ### Rule TRANS-003: Use Nested Structure for Organization ```json // ✅ GOOD: Organized by section { "common": { "save": "Sauvegarder", "cancel": "Annuler" }, "store": { "dashboard": { "title": "Tableau de bord" } }, "shop": { "cart": { "empty": "Votre panier est vide" } } } // ❌ BAD: Flat structure { "save": "Sauvegarder", "cancel": "Annuler", "dashboard_title": "Tableau de bord", "cart_empty": "Votre panier est vide" } ``` --- ## Quick Reference ### Language Selector Implementation Checklist - [ ] Function defined in appropriate JS file (`shop-layout.js` or `init-alpine.js`) - [ ] Function exported to `window.languageSelector` - [ ] Uses `tojson|safe` filter for language array - [ ] Provides default values for both parameters - [ ] Uses native language names (Français, Deutsch, Lëtzebuergesch) - [ ] Uses correct flag codes (en→gb, fr→fr, de→de, lb→lu) - [ ] Calls `/api/v1/platform/language/set` with POST method - [ ] Reloads page after successful language change - [ ] Hides selector if only one language enabled (storefront) - [ ] Shows all languages (store dashboard) ### Database Column Defaults | Table | Column | Type | Default | Nullable | |-------|--------|------|---------|----------| | stores | default_language | VARCHAR(5) | 'fr' | NO | | stores | dashboard_language | VARCHAR(5) | 'fr' | NO | | stores | storefront_language | VARCHAR(5) | 'fr' | NO | | stores | storefront_languages | JSON | ["fr","de","en"] | NO | | users | preferred_language | VARCHAR(5) | NULL | YES | | customers | preferred_language | VARCHAR(5) | NULL | YES | ### Files Requiring Language Support | File | Type | Notes | |------|------|-------| | `static/shop/js/shop-layout.js` | JS | `languageSelector()` for storefront | | `app/modules/core/static/store/js/init-alpine.js` | JS | `languageSelector()` for store dashboard | | `app/modules/core/static/admin/js/init-alpine.js` | JS | `languageSelector()` for admin | | `app/modules/core/static/merchant/js/init-alpine.js` | JS | `languageSelector()` for merchant | | `app/templates/store/partials/header.html` | Template | Store dashboard language selector | | `app/templates/admin/partials/header.html` | Template | Admin language selector | | `app/templates/merchant/partials/header.html` | Template | Merchant language selector | | `app/templates/shop/base.html` | Template | Storefront language selector | | `app/modules/core/routes/api/platform.py` | API | Language endpoints (`/api/v1/platform/language/*`) | | `middleware/language.py` | Middleware | Language detection per frontend type | | `static/locales/*.json` | JSON | Translation files | --- ## Common Violations and Fixes ### Violation: Alpine.js Expression Error **Symptom:** ``` Alpine Expression Error: expected expression, got '}' ``` **Cause:** Inline x-data with Jinja template variable not properly escaped. **Fix:** Move to function-based approach with `tojson|safe`. ### Violation: languageFlags is not defined **Symptom:** ``` Uncaught ReferenceError: languageFlags is not defined ``` **Cause:** `languageSelector` function not loaded or not exported. **Fix:** Ensure function is defined in JS file and exported to `window.languageSelector`. ### Violation: Wrong flag displayed **Symptom:** US flag shown for English, or no flag for Luxembourgish. **Cause:** Incorrect flag code mapping. **Fix:** Use correct mappings: `en→gb`, `fr→fr`, `de→de`, `lb→lu`. --- ## Validation Add these checks to CI/CD pipeline: ```bash # Validate translation files are valid JSON python -c "import json; json.load(open('static/locales/en.json'))" python -c "import json; json.load(open('static/locales/fr.json'))" python -c "import json; json.load(open('static/locales/de.json'))" python -c "import json; json.load(open('static/locales/lb.json'))" # Check all translation keys exist in all files python scripts/validate_translations.py ``` --- **Remember:** Language implementation errors cause immediate user-facing issues. Follow these rules strictly.