Files
orion/docs/architecture/language-i18n.md
Samir Boulahtit d2b05441fc 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>
2025-12-13 22:36:09 +01:00

15 KiB

Language & Internationalization (i18n) Architecture

This document defines strict rules for implementing language support across the Wizamart platform.

IMPORTANT: These rules are mandatory. Violations will cause runtime errors, inconsistent UX, or security issues.


Table of Contents

  1. Supported Languages
  2. Language Context Flow
  3. Database Schema Rules
  4. Frontend Rules
  5. API Rules
  6. Template Rules
  7. JavaScript Rules
  8. 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

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

Language is resolved in this order (highest to lowest priority):

  1. URL parameter (?lang=fr)
  2. Cookie (wizamart_language)
  3. User preference (database: preferred_language)
  4. Vendor default (database: storefront_language or dashboard_language)
  5. Accept-Language header (browser)
  6. Platform default (fr)

Vendor Dashboard vs Storefront

Context Language Source Database Field
Vendor Dashboard Vendor's dashboard_language vendors.dashboard_language
Customer Storefront Vendor's storefront_language vendors.storefront_language
Admin Panel User's preferred_language users.preferred_language

Database Schema Rules

Rule DB-001: Vendor Language Fields Are Required

Vendors MUST have these language columns with defaults:

# ✅ GOOD: All language fields with defaults
class Vendor(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"])
# ❌ BAD: Nullable language fields
class Vendor(Base):
    default_language = Column(String(5), nullable=True)  # ❌ Must have default

Rule DB-002: User/Customer Preferred Language Is Optional

# ✅ GOOD: Optional with fallback logic
class User(Base):
    preferred_language = Column(String(5), nullable=True)  # Falls back to vendor/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

# ✅ GOOD: Optional in response with defaults
class VendorResponse(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 VendorResponse(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.

<!-- ❌ BAD: Inline complex object with Jinja variable -->
<div x-data="{
    isLangOpen: false,
    currentLang: '{{ request.state.language }}',
    languages: {{ enabled_langs }},  <!-- ❌ Jinja outputs Python list, not JSON -->
    async setLanguage(lang) {
        // Complex function here
    }
}">
<!-- ✅ GOOD: Use function with tojson|safe filter -->
<div x-data="languageSelector('{{ request.state.language|default("fr") }}', {{ enabled_langs|tojson|safe }})">
// ✅ 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

<!-- ❌ BAD: Raw Jinja output -->
<div x-data="{ languages: {{ vendor.storefront_languages }} }">
<!-- Outputs: ['fr', 'de'] - Python syntax, invalid JavaScript -->

<!-- ❌ BAD: tojson without safe -->
<div x-data="{ languages: {{ vendor.storefront_languages|tojson }} }">
<!-- May escape quotes to &quot; in HTML context -->

<!-- ✅ GOOD: tojson with safe -->
<div x-data="{ languages: {{ vendor.storefront_languages|tojson|safe }} }">
<!-- Outputs: ["fr", "de"] - Valid JSON/JavaScript -->

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/vendor/js/init-alpine.js for vendor dashboard
// ✅ 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/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 Vendor's Enabled Languages

<!-- ✅ GOOD: Only show languages enabled by vendor -->
{% set enabled_langs = vendor.storefront_languages if vendor and vendor.storefront_languages else ['fr', 'de', 'en'] %}
{% if enabled_langs|length > 1 %}
<div x-data="languageSelector('{{ request.state.language|default("fr") }}', {{ enabled_langs|tojson|safe }})">
    <!-- Language selector UI -->
</div>
{% endif %}
<!-- ❌ BAD: Hardcoded language list ignoring vendor settings -->
<div x-data="languageSelector('fr', ['en', 'fr', 'de', 'lb'])">
    <!-- Shows all languages regardless of vendor config -->
</div>

Rule FE-005: Vendor Dashboard Shows All Languages

<!-- ✅ GOOD: Vendor dashboard always shows all 4 languages -->
<div x-data="languageSelector('{{ request.state.language|default("fr") }}', ['en', 'fr', 'de', 'lb'])">

API Rules

# ✅ 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="wizamart_language",
        value=request.language,
        max_age=365 * 24 * 60 * 60,  # 1 year
        httponly=True,
        samesite="lax"
    )
    return {"success": True, "language": request.language}

Rule API-002: Validate Language Codes

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

<!-- ✅ GOOD: Default value -->
{{ request.state.language|default("fr") }}

<!-- ❌ BAD: No default -->
{{ request.state.language }}  <!-- ❌ May be None -->

Rule TPL-002: Check Language Array Length Before Rendering Selector

<!-- ✅ GOOD: Only show if multiple languages -->
{% if enabled_langs|length > 1 %}
    <!-- Language selector -->
{% endif %}

<!-- ❌ BAD: Always show even with single language -->
<!-- Language selector shown with only 1 option -->

Rule TPL-003: Use Consistent Flag Icon Classes

<!-- ✅ GOOD: Use flag-icons library consistently -->
<span class="fi fi-{{ flag_code }}"></span>

<!-- Flag codes mapping -->
<!-- en → gb (Great Britain) -->
<!-- fr → fr (France) -->
<!-- de → de (Germany) -->
<!-- lb → lu (Luxembourg) -->

JavaScript Rules

Rule JS-001: Language Names Must Use Native Language

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

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

// ✅ GOOD: Correct endpoint
fetch('/api/v1/language/set', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ language: lang })
});

// ❌ BAD: Wrong endpoint or method
fetch('/api/language/set', ...);     // ❌ Missing /v1
fetch('/api/v1/language', ...);      // ❌ Missing /set
fetch('/api/v1/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

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

// ✅ GOOD: Organized by section
{
    "common": {
        "save": "Sauvegarder",
        "cancel": "Annuler"
    },
    "vendor": {
        "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/language/set with POST method
  • Reloads page after successful language change
  • Hides selector if only one language enabled (storefront)
  • Shows all languages (vendor dashboard)

Database Column Defaults

Table Column Type Default Nullable
vendors default_language VARCHAR(5) 'fr' NO
vendors dashboard_language VARCHAR(5) 'fr' NO
vendors storefront_language VARCHAR(5) 'fr' NO
vendors 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() function
static/vendor/js/init-alpine.js JS languageSelector() function
app/templates/shop/base.html Template Storefront language selector
app/templates/vendor/partials/header.html Template Dashboard language selector
app/api/v1/shared/language.py API Language endpoints
middleware/language.py Middleware Language detection
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:

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