feat: add language switching to admin and merchant frontends
Some checks failed
Some checks failed
- Add cookie to ADMIN resolution chain (cookie → user_pref → "en") - Add explicit MERCHANT resolution (cookie → user_pref → "fr") - Add language selector dropdown to admin and merchant headers - Add languageSelector() function to merchant init-alpine.js - Add flag-icons CSS and i18n.js setup to merchant base template - Add compact flag-based language selector to both login pages - Make lang attribute dynamic on all base and login templates - Pass current_language to login route template context - Update architecture doc with ADMIN/MERCHANT resolution priorities Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,10 @@ async def admin_login_page(
|
|||||||
if current_user:
|
if current_user:
|
||||||
return RedirectResponse(url="/admin/dashboard", status_code=302)
|
return RedirectResponse(url="/admin/dashboard", status_code=302)
|
||||||
|
|
||||||
return templates.TemplateResponse("tenancy/admin/login.html", {"request": request})
|
return templates.TemplateResponse("tenancy/admin/login.html", {
|
||||||
|
"request": request,
|
||||||
|
"current_language": getattr(request.state, "language", "en"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/select-platform", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/select-platform", response_class=HTMLResponse, include_in_schema=False)
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ async def merchant_login_page(
|
|||||||
if current_user:
|
if current_user:
|
||||||
return RedirectResponse(url="/merchants/dashboard", status_code=302)
|
return RedirectResponse(url="/merchants/dashboard", status_code=302)
|
||||||
|
|
||||||
return templates.TemplateResponse("tenancy/merchant/login.html", {"request": request})
|
return templates.TemplateResponse("tenancy/merchant/login.html", {
|
||||||
|
"request": request,
|
||||||
|
"current_language": getattr(request.state, "language", "fr"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -201,3 +201,49 @@ function data() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language Selector Component
|
||||||
|
* Alpine.js component for language switching in merchant portal
|
||||||
|
*/
|
||||||
|
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/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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.languageSelector = languageSelector;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{# app/templates/admin/login.html #}
|
{# app/templates/admin/login.html #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html :class="{ 'dark': dark }" x-data="adminLogin()" lang="en">
|
<html :class="{ 'dark': dark }" x-data="adminLogin()" lang="{{ current_language|default('en') }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -9,6 +9,12 @@
|
|||||||
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
|
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />
|
||||||
|
<!-- Flag Icons CSS (for language selector) with local fallback -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"
|
||||||
|
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/store/flag-icons.min.css') }}';"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
</style>
|
</style>
|
||||||
@@ -97,6 +103,23 @@
|
|||||||
← Back to Platform
|
← Back to Platform
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Language selector -->
|
||||||
|
<div class="flex items-center justify-center gap-3 mt-6" x-data="{
|
||||||
|
async setLang(lang) {
|
||||||
|
await fetch('/api/v1/platform/language/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({language: lang})
|
||||||
|
});
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<button @click="setLang('en')" class="fi fi-gb text-lg opacity-60 hover:opacity-100 transition-opacity" title="English"></button>
|
||||||
|
<button @click="setLang('fr')" class="fi fi-fr text-lg opacity-60 hover:opacity-100 transition-opacity" title="Français"></button>
|
||||||
|
<button @click="setLang('de')" class="fi fi-de text-lg opacity-60 hover:opacity-100 transition-opacity" title="Deutsch"></button>
|
||||||
|
<button @click="setLang('lb')" class="fi fi-lu text-lg opacity-60 hover:opacity-100 transition-opacity" title="Lëtzebuergesch"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{# app/modules/tenancy/templates/tenancy/merchant/login.html #}
|
{# app/modules/tenancy/templates/tenancy/merchant/login.html #}
|
||||||
{# Standalone login page - does NOT extend merchant/base.html #}
|
{# Standalone login page - does NOT extend merchant/base.html #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html :class="{ 'dark': dark }" x-data="merchantLogin()" lang="en">
|
<html :class="{ 'dark': dark }" x-data="merchantLogin()" lang="{{ current_language|default('fr') }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -10,6 +10,12 @@
|
|||||||
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
|
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='merchant/css/tailwind.output.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', path='merchant/css/tailwind.output.css') }}" />
|
||||||
|
<!-- Flag Icons CSS (for language selector) with local fallback -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"
|
||||||
|
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/store/flag-icons.min.css') }}';"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
</style>
|
</style>
|
||||||
@@ -128,6 +134,23 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Language selector -->
|
||||||
|
<div class="flex items-center justify-center gap-3 mt-6" x-data="{
|
||||||
|
async setLang(lang) {
|
||||||
|
await fetch('/api/v1/platform/language/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({language: lang})
|
||||||
|
});
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<button @click="setLang('en')" class="fi fi-gb text-lg opacity-60 hover:opacity-100 transition-opacity" title="English"></button>
|
||||||
|
<button @click="setLang('fr')" class="fi fi-fr text-lg opacity-60 hover:opacity-100 transition-opacity" title="Français"></button>
|
||||||
|
<button @click="setLang('de')" class="fi fi-de text-lg opacity-60 hover:opacity-100 transition-opacity" title="Deutsch"></button>
|
||||||
|
<button @click="setLang('lb')" class="fi fi-lu text-lg opacity-60 hover:opacity-100 transition-opacity" title="Lëtzebuergesch"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{# app/templates/admin/base.html #}
|
{# app/templates/admin/base.html #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html :class="{ 'dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
|
<html :class="{ 'dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="{{ current_language|default('en') }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -50,6 +50,44 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Language selector -->
|
||||||
|
<li class="relative" x-data="languageSelector('{{ request.state.language|default('en') }}')">
|
||||||
|
<button
|
||||||
|
@click="isLangOpen = !isLangOpen"
|
||||||
|
@click.outside="isLangOpen = false"
|
||||||
|
class="p-1 rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||||
|
aria-label="Change language"
|
||||||
|
>
|
||||||
|
<span class="fi text-lg" :class="'fi-' + languageFlags[currentLang]"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
x-show="isLangOpen"
|
||||||
|
x-cloak
|
||||||
|
x-transition:enter="transition ease-out duration-100"
|
||||||
|
x-transition:enter-start="transform opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="transform opacity-0 scale-95"
|
||||||
|
class="absolute right-0 w-44 mt-2 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-100 dark:border-gray-600 py-1 z-50"
|
||||||
|
>
|
||||||
|
<template x-for="lang in languages" :key="lang">
|
||||||
|
<button
|
||||||
|
@click="setLanguage(lang)"
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-3 w-full px-4 py-2 text-sm font-medium transition-colors"
|
||||||
|
:class="currentLang === lang
|
||||||
|
? 'bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300'
|
||||||
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'"
|
||||||
|
>
|
||||||
|
<span class="fi" :class="'fi-' + languageFlags[lang]"></span>
|
||||||
|
<span x-text="languageNames[lang]"></span>
|
||||||
|
<span x-show="currentLang === lang" x-html="$icon('check', 'w-4 h-4 ml-auto')" class="text-purple-600 dark:text-purple-400"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Messages link with badge -->
|
<!-- Messages link with badge -->
|
||||||
<li class="relative" x-data="headerMessages()">
|
<li class="relative" x-data="headerMessages()">
|
||||||
<a href="/admin/messages"
|
<a href="/admin/messages"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{# app/templates/merchant/base.html #}
|
{# app/templates/merchant/base.html #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html :class="{ 'dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
|
<html :class="{ 'dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="{{ current_language|default('fr') }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -13,6 +13,13 @@
|
|||||||
<!-- Tailwind CSS v4 (built locally via standalone CLI) -->
|
<!-- Tailwind CSS v4 (built locally via standalone CLI) -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='merchant/css/tailwind.output.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', path='merchant/css/tailwind.output.css') }}" />
|
||||||
|
|
||||||
|
<!-- Flag Icons CSS (for language selector) with local fallback -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"
|
||||||
|
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/store/flag-icons.min.css') }}';"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Alpine Cloak -->
|
<!-- Alpine Cloak -->
|
||||||
<style>
|
<style>
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
@@ -52,6 +59,15 @@
|
|||||||
<!-- 4. FOURTH: Utils (standalone utilities) -->
|
<!-- 4. FOURTH: Utils (standalone utilities) -->
|
||||||
<script defer src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
|
<script defer src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- 4b. i18n Support -->
|
||||||
|
<script defer src="{{ url_for('static', path='shared/js/i18n.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const modules = {% block i18n_modules %}[]{% endblock %};
|
||||||
|
await I18n.init('{{ current_language | default("fr") }}', modules);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- 5. FIFTH: API Client (depends on Utils) -->
|
<!-- 5. FIFTH: API Client (depends on Utils) -->
|
||||||
<script defer src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
<script defer src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,44 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Language selector -->
|
||||||
|
<li class="relative" x-data="languageSelector('{{ request.state.language|default('fr') }}')">
|
||||||
|
<button
|
||||||
|
@click="isLangOpen = !isLangOpen"
|
||||||
|
@click.outside="isLangOpen = false"
|
||||||
|
class="p-1 rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||||
|
aria-label="Change language"
|
||||||
|
>
|
||||||
|
<span class="fi text-lg" :class="'fi-' + languageFlags[currentLang]"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
x-show="isLangOpen"
|
||||||
|
x-cloak
|
||||||
|
x-transition:enter="transition ease-out duration-100"
|
||||||
|
x-transition:enter-start="transform opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="transform opacity-0 scale-95"
|
||||||
|
class="absolute right-0 w-44 mt-2 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-100 dark:border-gray-600 py-1 z-50"
|
||||||
|
>
|
||||||
|
<template x-for="lang in languages" :key="lang">
|
||||||
|
<button
|
||||||
|
@click="setLanguage(lang)"
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-3 w-full px-4 py-2 text-sm font-medium transition-colors"
|
||||||
|
:class="currentLang === lang
|
||||||
|
? 'bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300'
|
||||||
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'"
|
||||||
|
>
|
||||||
|
<span class="fi" :class="'fi-' + languageFlags[lang]"></span>
|
||||||
|
<span x-text="languageNames[lang]"></span>
|
||||||
|
<span x-show="currentLang === lang" x-html="$icon('check', 'w-4 h-4 ml-auto')" class="text-purple-600 dark:text-purple-400"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Profile menu -->
|
<!-- Profile menu -->
|
||||||
<li class="relative" x-data="{ profileOpen: false }">
|
<li class="relative" x-data="{ profileOpen: false }">
|
||||||
<button class="align-middle rounded-full focus:shadow-outline-purple focus:outline-none"
|
<button class="align-middle rounded-full focus:shadow-outline-purple focus:outline-none"
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ Language resolution varies by frontend type. Each chain is evaluated top-to-bott
|
|||||||
|
|
||||||
| Frontend | Priority (highest → lowest) |
|
| Frontend | Priority (highest → lowest) |
|
||||||
|----------|----------------------------|
|
|----------|----------------------------|
|
||||||
| **ADMIN** | User `preferred_language` → `"en"` |
|
| **ADMIN** | Cookie (`lang`) → User `preferred_language` → `"en"` |
|
||||||
|
| **MERCHANT** | Cookie (`lang`) → User `preferred_language` → `"fr"` |
|
||||||
| **STORE** | Cookie (`lang`) → User `preferred_language` → Store `dashboard_language` → `"fr"` |
|
| **STORE** | Cookie (`lang`) → User `preferred_language` → Store `dashboard_language` → `"fr"` |
|
||||||
| **STOREFRONT** | Customer `preferred_language` → Cookie (`lang`) → Store `storefront_language` → Browser `Accept-Language` → `"fr"` |
|
| **STOREFRONT** | Customer `preferred_language` → Cookie (`lang`) → Store `storefront_language` → Browser `Accept-Language` → `"fr"` |
|
||||||
| **PLATFORM** | Cookie (`lang`) → Browser `Accept-Language` → `"fr"` |
|
| **PLATFORM** | Cookie (`lang`) → Browser `Accept-Language` → `"fr"` |
|
||||||
@@ -61,9 +62,10 @@ The **cookie** (`lang`) is set by the language switcher UI via `POST /api/v1/pla
|
|||||||
|
|
||||||
| Context | Language Source | Database Field |
|
| Context | Language Source | Database Field |
|
||||||
|---------|-----------------|----------------|
|
|---------|-----------------|----------------|
|
||||||
|
| Admin Panel | User's `preferred_language` | `users.preferred_language` |
|
||||||
|
| Merchant Portal | User's `preferred_language` | `users.preferred_language` |
|
||||||
| Store Dashboard | Store's `dashboard_language` | `stores.dashboard_language` |
|
| Store Dashboard | Store's `dashboard_language` | `stores.dashboard_language` |
|
||||||
| Customer Storefront | Store's `storefront_language` | `stores.storefront_language` |
|
| Customer Storefront | Store's `storefront_language` | `stores.storefront_language` |
|
||||||
| Admin Panel | User's `preferred_language` | `users.preferred_language` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -490,12 +492,16 @@ static/locales/
|
|||||||
|
|
||||||
| File | Type | Notes |
|
| File | Type | Notes |
|
||||||
|------|------|-------|
|
|------|------|-------|
|
||||||
| `static/shop/js/shop-layout.js` | JS | `languageSelector()` function |
|
| `static/shop/js/shop-layout.js` | JS | `languageSelector()` for storefront |
|
||||||
| `static/store/js/init-alpine.js` | JS | `languageSelector()` function |
|
| `app/modules/core/static/store/js/init-alpine.js` | JS | `languageSelector()` for store dashboard |
|
||||||
|
| `app/modules/core/static/admin/js/init-alpine.js` | JS | `languageSelector()` for admin |
|
||||||
|
| `app/modules/core/static/merchant/js/init-alpine.js` | JS | `languageSelector()` for merchant |
|
||||||
|
| `app/templates/store/partials/header.html` | Template | Store dashboard language selector |
|
||||||
|
| `app/templates/admin/partials/header.html` | Template | Admin language selector |
|
||||||
|
| `app/templates/merchant/partials/header.html` | Template | Merchant language selector |
|
||||||
| `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/modules/core/routes/api/platform.py` | API | Language endpoints (`/api/v1/platform/language/*`) |
|
| `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 per frontend type |
|
||||||
| `static/locales/*.json` | JSON | Translation files |
|
| `static/locales/*.json` | JSON | Translation files |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -38,10 +38,11 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
|||||||
Middleware to detect and set the request language.
|
Middleware to detect and set the request language.
|
||||||
|
|
||||||
Sets request.state.language based on context:
|
Sets request.state.language based on context:
|
||||||
- Admin: Always English (for now)
|
- Admin: Cookie → User preference → English
|
||||||
- Store dashboard: User preference → Store dashboard_language → default
|
- Merchant: Cookie → User preference → Platform default
|
||||||
|
- Store dashboard: Cookie → User preference → Store dashboard_language → default
|
||||||
- Storefront: Customer preference → Cookie → Store storefront_language → browser → default
|
- Storefront: Customer preference → Cookie → Store storefront_language → browser → default
|
||||||
- API: Accept-Language header → default
|
- Platform: Cookie → Browser → Platform default
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next) -> Response:
|
async def dispatch(self, request: Request, call_next) -> Response:
|
||||||
@@ -61,9 +62,9 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
# Resolve language based on frontend type
|
# Resolve language based on frontend type
|
||||||
if frontend_type == FrontendType.ADMIN:
|
if frontend_type == FrontendType.ADMIN:
|
||||||
# Admin dashboard: respect user's preferred language
|
# Admin dashboard: cookie → user preference → English default
|
||||||
user_preferred = self._get_user_language_from_token(request)
|
user_preferred = self._get_user_language_from_token(request)
|
||||||
language = user_preferred or "en"
|
language = cookie_language or user_preferred or "en"
|
||||||
|
|
||||||
elif frontend_type == FrontendType.STORE:
|
elif frontend_type == FrontendType.STORE:
|
||||||
# Store dashboard
|
# Store dashboard
|
||||||
@@ -90,6 +91,11 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
|||||||
enabled_languages=enabled_languages,
|
enabled_languages=enabled_languages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif frontend_type == FrontendType.MERCHANT:
|
||||||
|
# Merchant portal: cookie → user preference → platform default
|
||||||
|
user_preferred = self._get_user_language_from_token(request)
|
||||||
|
language = cookie_language or user_preferred or DEFAULT_LANGUAGE
|
||||||
|
|
||||||
elif frontend_type == FrontendType.PLATFORM:
|
elif frontend_type == FrontendType.PLATFORM:
|
||||||
# Platform marketing pages: Use cookie, browser, or default
|
# Platform marketing pages: Use cookie, browser, or default
|
||||||
language = cookie_language or browser_language or DEFAULT_LANGUAGE
|
language = cookie_language or browser_language or DEFAULT_LANGUAGE
|
||||||
|
|||||||
Reference in New Issue
Block a user