# 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
<!-- ❌ 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
    }
}">
```

```html
<!-- ✅ GOOD: Use function with tojson|safe filter -->
<div x-data="languageSelector('{{ request.state.language|default("fr") }}', {{ enabled_langs|tojson|safe }})">
```

```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
<!-- ❌ BAD: Raw Jinja output -->
<div x-data="{ languages: {{ store.storefront_languages }} }">
<!-- Outputs: ['fr', 'de'] - Python syntax, invalid JavaScript -->

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

<!-- ✅ GOOD: tojson with safe -->
<div x-data="{ languages: {{ store.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/storefront/js/storefront-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
<!-- ✅ GOOD: Only show languages enabled by store -->
{% set enabled_langs = store.storefront_languages if store and store.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 %}
```

```html
<!-- ❌ BAD: Hardcoded language list ignoring store settings -->
<div x-data="languageSelector('fr', ['en', 'fr', 'de', 'lb'])">
    <!-- Shows all languages regardless of store config -->
</div>
```

### Rule FE-005: Store Dashboard Shows All Languages

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

---

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

```html
<!-- ✅ 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

```html
<!-- ✅ 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

```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"
        }
    },
    "storefront": {
        "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 (`storefront-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/storefront/js/storefront-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/storefront/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.
