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>
15 KiB
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
- Supported Languages
- Language Context Flow
- Database Schema Rules
- Frontend Rules
- API Rules
- Template Rules
- JavaScript 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
# ✅ 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):
- URL parameter (
?lang=fr) - Cookie (
orion_language) - User preference (database:
preferred_language) - Store default (database:
storefront_languageordashboard_language) - Accept-Language header (browser)
- 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:
# ✅ 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"])
# ❌ 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
# ✅ 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
# ✅ 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.
<!-- ❌ 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: {{ 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.jsfor storefrontstatic/store/js/init-alpine.jsfor store 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 Store's Enabled Languages
<!-- ✅ 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 %}
<!-- ❌ 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
<!-- ✅ 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
# ✅ 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
# ✅ 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"
},
"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.jsorinit-alpine.js) - Function exported to
window.languageSelector - Uses
tojson|safefilter 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/setwith 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:
# 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.