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>
552 lines
15 KiB
Markdown
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 " 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.
|