fix: language switcher stuck in French on store dashboard
Some checks failed
CI / ruff (push) Successful in 11s
CI / pytest (push) Failing after 44m35s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 28s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

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:
2026-02-24 05:30:25 +01:00
parent cd935988c4
commit 0389294b1a
4 changed files with 35 additions and 51 deletions

View File

@@ -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"
@@ -179,4 +153,4 @@
</li> </li>
</ul> </ul>
</div> </div>
</header> </header>

View File

@@ -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

View File

@@ -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 |

View File

@@ -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