fix: language switcher stuck in French on store dashboard
Some checks failed
Some checks failed
Three compounding bugs prevented language switching on the store dashboard: - Cookie missing path="/", scoping it to the API endpoint path only - STORE frontend resolution chain ignored the cookie entirely - Store header used inline x-data with wrong language names instead of shared languageSelector() Also updates architecture doc with correct per-frontend resolution priorities, cookie name, API endpoint path, and file references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,33 +37,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Language selector -->
|
<!-- Language selector -->
|
||||||
<li class="relative" x-data="{
|
<li class="relative" x-data="languageSelector('{{ request.state.language|default('fr') }}')">
|
||||||
isLangOpen: false,
|
|
||||||
currentLang: '{{ request.state.language|default("fr") }}',
|
|
||||||
languages: ['en', 'fr', 'de', 'lb'],
|
|
||||||
languageNames: { 'en': 'English', 'fr': 'Francais', 'de': 'Deutsch', 'lb': 'Letzebuerg' },
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<button
|
<button
|
||||||
@click="isLangOpen = !isLangOpen"
|
@click="isLangOpen = !isLangOpen"
|
||||||
@click.outside="isLangOpen = false"
|
@click.outside="isLangOpen = false"
|
||||||
|
|||||||
@@ -351,15 +351,20 @@ def get_jinja2_globals(language: str = None) -> dict:
|
|||||||
def resolve_store_dashboard_language(
|
def resolve_store_dashboard_language(
|
||||||
user_preferred: str | None,
|
user_preferred: str | None,
|
||||||
store_dashboard: str | None,
|
store_dashboard: str | None,
|
||||||
|
cookie_language: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Resolve language for store dashboard.
|
Resolve language for store dashboard.
|
||||||
|
|
||||||
Priority:
|
Priority:
|
||||||
1. User's preferred_language (if set)
|
1. Cookie (explicit UI action via language switcher)
|
||||||
2. Store's dashboard_language
|
2. User's preferred_language (DB preference from profile settings)
|
||||||
3. System default (fr)
|
3. Store's dashboard_language
|
||||||
|
4. System default (fr)
|
||||||
"""
|
"""
|
||||||
|
if cookie_language and cookie_language in SUPPORTED_LANGUAGES:
|
||||||
|
return cookie_language
|
||||||
|
|
||||||
if user_preferred and user_preferred in SUPPORTED_LANGUAGES:
|
if user_preferred and user_preferred in SUPPORTED_LANGUAGES:
|
||||||
return user_preferred
|
return user_preferred
|
||||||
|
|
||||||
|
|||||||
@@ -44,18 +44,20 @@ language = "lux" # ❌ Use "lb"
|
|||||||
|
|
||||||
## Language Context Flow
|
## Language Context Flow
|
||||||
|
|
||||||
### Resolution Priority
|
### Resolution Priority (per Frontend Type)
|
||||||
|
|
||||||
Language is resolved in this order (highest to lowest priority):
|
Language resolution varies by frontend type. Each chain is evaluated top-to-bottom, first match wins:
|
||||||
|
|
||||||
1. **URL parameter** (`?lang=fr`)
|
| Frontend | Priority (highest → lowest) |
|
||||||
2. **Cookie** (`orion_language`)
|
|----------|----------------------------|
|
||||||
3. **User preference** (database: `preferred_language`)
|
| **ADMIN** | User `preferred_language` → `"en"` |
|
||||||
4. **Store default** (database: `storefront_language` or `dashboard_language`)
|
| **STORE** | Cookie (`lang`) → User `preferred_language` → Store `dashboard_language` → `"fr"` |
|
||||||
5. **Accept-Language header** (browser)
|
| **STOREFRONT** | Customer `preferred_language` → Cookie (`lang`) → Store `storefront_language` → Browser `Accept-Language` → `"fr"` |
|
||||||
6. **Platform default** (`fr`)
|
| **PLATFORM** | Cookie (`lang`) → Browser `Accept-Language` → `"fr"` |
|
||||||
|
|
||||||
### Store Dashboard vs Storefront
|
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 |
|
| Context | Language Source | Database Field |
|
||||||
|---------|-----------------|----------------|
|
|---------|-----------------|----------------|
|
||||||
@@ -210,7 +212,7 @@ function languageSelector(currentLang, enabledLanguages) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/language/set', {
|
const response = await fetch('/api/v1/platform/language/set', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ language: lang })
|
body: JSON.stringify({ language: lang })
|
||||||
@@ -270,11 +272,12 @@ async def set_language(request: LanguageSetRequest, response: Response):
|
|||||||
raise HTTPException(status_code=400, detail="Unsupported language")
|
raise HTTPException(status_code=400, detail="Unsupported language")
|
||||||
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="orion_language",
|
key="lang",
|
||||||
value=request.language,
|
value=request.language,
|
||||||
max_age=365 * 24 * 60 * 60, # 1 year
|
max_age=365 * 24 * 60 * 60, # 1 year
|
||||||
httponly=True,
|
httponly=False, # Accessible to JavaScript
|
||||||
samesite="lax"
|
samesite="lax",
|
||||||
|
path="/", # Must be site-wide
|
||||||
)
|
)
|
||||||
return {"success": True, "language": request.language}
|
return {"success": True, "language": request.language}
|
||||||
```
|
```
|
||||||
@@ -379,16 +382,16 @@ languageFlags: {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ✅ GOOD: Correct endpoint
|
// ✅ GOOD: Correct endpoint
|
||||||
fetch('/api/v1/language/set', {
|
fetch('/api/v1/platform/language/set', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ language: lang })
|
body: JSON.stringify({ language: lang })
|
||||||
});
|
});
|
||||||
|
|
||||||
// ❌ BAD: Wrong endpoint or method
|
// ❌ BAD: Wrong endpoint or method
|
||||||
fetch('/api/language/set', ...); // ❌ Missing /v1
|
fetch('/api/v1/language/set', ...); // ❌ Missing /platform
|
||||||
fetch('/api/v1/language', ...); // ❌ Missing /set
|
fetch('/api/v1/platform/language', ...); // ❌ Missing /set
|
||||||
fetch('/api/v1/language/set', { method: 'GET' }); // ❌ Should be POST
|
fetch('/api/v1/platform/language/set', { method: 'GET' }); // ❌ Should be POST
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -467,7 +470,7 @@ static/locales/
|
|||||||
- [ ] Provides default values for both parameters
|
- [ ] Provides default values for both parameters
|
||||||
- [ ] Uses native language names (Français, Deutsch, Lëtzebuergesch)
|
- [ ] Uses native language names (Français, Deutsch, Lëtzebuergesch)
|
||||||
- [ ] Uses correct flag codes (en→gb, fr→fr, de→de, lb→lu)
|
- [ ] Uses correct flag codes (en→gb, fr→fr, de→de, lb→lu)
|
||||||
- [ ] Calls `/api/v1/language/set` with POST method
|
- [ ] Calls `/api/v1/platform/language/set` with POST method
|
||||||
- [ ] Reloads page after successful language change
|
- [ ] Reloads page after successful language change
|
||||||
- [ ] Hides selector if only one language enabled (storefront)
|
- [ ] Hides selector if only one language enabled (storefront)
|
||||||
- [ ] Shows all languages (store dashboard)
|
- [ ] Shows all languages (store dashboard)
|
||||||
@@ -491,7 +494,7 @@ static/locales/
|
|||||||
| `static/store/js/init-alpine.js` | JS | `languageSelector()` function |
|
| `static/store/js/init-alpine.js` | JS | `languageSelector()` function |
|
||||||
| `app/templates/shop/base.html` | Template | Storefront language selector |
|
| `app/templates/shop/base.html` | Template | Storefront language selector |
|
||||||
| `app/templates/store/partials/header.html` | Template | Dashboard language selector |
|
| `app/templates/store/partials/header.html` | Template | Dashboard language selector |
|
||||||
| `app/api/v1/shared/language.py` | API | Language endpoints |
|
| `app/modules/core/routes/api/platform.py` | API | Language endpoints (`/api/v1/platform/language/*`) |
|
||||||
| `middleware/language.py` | Middleware | Language detection |
|
| `middleware/language.py` | Middleware | Language detection |
|
||||||
| `static/locales/*.json` | JSON | Translation files |
|
| `static/locales/*.json` | JSON | Translation files |
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
|||||||
language = resolve_store_dashboard_language(
|
language = resolve_store_dashboard_language(
|
||||||
user_preferred=user_preferred,
|
user_preferred=user_preferred,
|
||||||
store_dashboard=store_dashboard,
|
store_dashboard=store_dashboard,
|
||||||
|
cookie_language=cookie_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif frontend_type == FrontendType.STOREFRONT:
|
elif frontend_type == FrontendType.STOREFRONT:
|
||||||
@@ -170,6 +171,7 @@ def set_language_cookie(response: Response, language: str) -> Response:
|
|||||||
max_age=60 * 60 * 24 * 365, # 1 year
|
max_age=60 * 60 * 24 * 365, # 1 year
|
||||||
httponly=False, # Accessible to JavaScript
|
httponly=False, # Accessible to JavaScript
|
||||||
samesite="lax",
|
samesite="lax",
|
||||||
|
path="/",
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -184,5 +186,5 @@ def delete_language_cookie(response: Response) -> Response:
|
|||||||
Returns:
|
Returns:
|
||||||
Modified response with cookie deleted
|
Modified response with cookie deleted
|
||||||
"""
|
"""
|
||||||
response.delete_cookie(key=LANGUAGE_COOKIE_NAME)
|
response.delete_cookie(key=LANGUAGE_COOKIE_NAME, path="/")
|
||||||
return response
|
return response
|
||||||
|
|||||||
Reference in New Issue
Block a user