feat: add language switching to admin and merchant frontends
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 46m27s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- 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:
2026-02-24 10:26:57 +01:00
parent 0389294b1a
commit 6c78827c7f
11 changed files with 219 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
{# app/templates/admin/base.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>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View File

@@ -50,6 +50,44 @@
</button>
</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 -->
<li class="relative" x-data="headerMessages()">
<a href="/admin/messages"

View File

@@ -1,6 +1,6 @@
{# app/templates/merchant/base.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>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -13,6 +13,13 @@
<!-- Tailwind CSS v4 (built locally via standalone CLI) -->
<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 -->
<style>
[x-cloak] { display: none !important; }
@@ -52,6 +59,15 @@
<!-- 4. FOURTH: Utils (standalone utilities) -->
<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) -->
<script defer src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>

View File

@@ -26,6 +26,44 @@
</button>
</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 -->
<li class="relative" x-data="{ profileOpen: false }">
<button class="align-middle rounded-full focus:shadow-outline-purple focus:outline-none"