Files
orion/docs/architecture/language-i18n.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

552 lines
15 KiB
Markdown

# 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
Language is resolved in this order (highest to lowest priority):
1. **URL parameter** (`?lang=fr`)
2. **Cookie** (`orion_language`)
3. **User preference** (database: `preferred_language`)
4. **Store default** (database: `storefront_language` or `dashboard_language`)
5. **Accept-Language header** (browser)
6. **Platform default** (`fr`)
### Store Dashboard vs Storefront
| Context | Language Source | Database Field |
|---------|-----------------|----------------|
| Store Dashboard | Store's `dashboard_language` | `stores.dashboard_language` |
| Customer Storefront | Store's `storefront_language` | `stores.storefront_language` |
| Admin Panel | User's `preferred_language` | `users.preferred_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/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/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="orion_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
```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/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
```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/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()` function |
| `static/store/js/init-alpine.js` | JS | `languageSelector()` function |
| `app/templates/shop/base.html` | Template | Storefront language selector |
| `app/templates/store/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:
```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.