feat: add multi-language (i18n) support for vendor dashboard and storefront

- Add database fields for language preferences:
  - Vendor: dashboard_language, storefront_language, storefront_languages
  - User: preferred_language
  - Customer: preferred_language

- Add language middleware for request-level language detection:
  - Cookie-based persistence
  - Browser Accept-Language fallback
  - Vendor storefront language constraints

- Add language API endpoints (/api/v1/language/*):
  - POST /set - Set language preference
  - GET /current - Get current language info
  - GET /list - List available languages
  - DELETE /clear - Clear preference

- Add i18n utilities (app/utils/i18n.py):
  - JSON-based translation loading
  - Jinja2 template integration
  - Language resolution helpers

- Add reusable language selector macros for templates
- Add languageSelector() Alpine.js component
- Add translation files (en, fr, de, lb) in static/locales/
- Add architecture rules documentation for language implementation
- Update marketplace-product-detail.js to use native language names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 22:36:09 +01:00
parent d21cd366dc
commit d2b05441fc
30 changed files with 4615 additions and 33 deletions

View File

@@ -0,0 +1,329 @@
{#
Language Selector Macros
========================
Reusable language selector components for vendor dashboard and storefront.
Usage:
{% from 'shared/macros/language_selector.html' import language_selector, language_selector_compact %}
{# Full language selector with labels #}
{{ language_selector(current_language='fr', enabled_languages=['fr', 'de', 'en'], position='right') }}
{# Compact selector (flag only) #}
{{ language_selector_compact(current_language='fr', enabled_languages=['fr', 'de', 'en']) }}
#}
{#
Language configuration - matches app/utils/i18n.py
Uses native language names per LANG-005 architecture rule
#}
{% set LANGUAGE_NAMES = {
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'lb': 'Lëtzebuergesch'
} %}
{% set LANGUAGE_NATIVE = {
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'lb': 'Lëtzebuergesch'
} %}
{% set LANGUAGE_FLAGS = {
'en': 'gb',
'fr': 'fr',
'de': 'de',
'lb': 'lu'
} %}
{#
Language Selector (Full)
========================
A dropdown language selector showing flag and language name.
Parameters:
- current_language: Current language code (default: 'fr')
- enabled_languages: List of enabled language codes (default: all)
- position: 'left' | 'right' (default: 'right')
- context: 'vendor' | 'shop' | 'admin' (affects API endpoint)
- show_label: Show language name next to flag (default: true)
#}
{% macro language_selector(current_language='fr', enabled_languages=none, position='right', context='shop', show_label=true) %}
{% set langs = enabled_languages or ['en', 'fr', 'de', 'lb'] %}
{% set current = current_language if current_language in langs else langs[0] %}
{% set positions = {'left': 'left-0', 'right': 'right-0'} %}
{# Uses languageSelector() function per LANG-002 architecture rule #}
<div
x-data="languageSelector('{{ current }}', {{ langs | tojson | safe }})"
class="relative inline-block"
>
<button
@click="isLangOpen = !isLangOpen"
@click.outside="isLangOpen = false"
type="button"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors"
:class="{ 'ring-2 ring-purple-500 ring-offset-2': isLangOpen }"
>
<span class="fi" :class="'fi-' + languageFlags[currentLang]"></span>
{% if show_label %}
<span x-text="languageNames[currentLang]" class="hidden sm:inline"></span>
{% endif %}
<span x-html="$icon('chevron-down', 'w-4 h-4')" :class="{ 'rotate-180': isLangOpen }" class="transition-transform"></span>
</button>
<div
x-show="isLangOpen"
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 z-50 mt-2 w-44 {{ positions[position] }} bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1"
>
<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-700'"
>
<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>
</div>
{% endmacro %}
{#
Language Selector (Compact)
===========================
A compact language selector showing only flag icon.
Good for headers with limited space.
Parameters:
- current_language: Current language code (default: 'fr')
- enabled_languages: List of enabled language codes (default: all)
- position: 'left' | 'right' (default: 'right')
#}
{% macro language_selector_compact(current_language='fr', enabled_languages=none, position='right') %}
{% set langs = enabled_languages or ['en', 'fr', 'de', 'lb'] %}
{% set current = current_language if current_language in langs else langs[0] %}
{% set positions = {'left': 'left-0', 'right': 'right-0'} %}
{# Uses languageSelector() function per LANG-002 architecture rule #}
<div
x-data="languageSelector('{{ current }}', {{ langs | tojson | safe }})"
class="relative"
>
<button
@click="isLangOpen = !isLangOpen"
@click.outside="isLangOpen = false"
type="button"
class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors focus:outline-none"
:class="{ 'text-gray-700 dark:text-white bg-gray-100 dark:bg-gray-700': isLangOpen }"
title="Change language"
>
<span class="fi text-lg" :class="'fi-' + languageFlags[currentLang]"></span>
</button>
<div
x-show="isLangOpen"
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 z-50 mt-1 w-44 {{ positions[position] }} bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1"
>
<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-700'"
>
<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>
</div>
{% endmacro %}
{#
Language Toggle (Simple)
========================
A simple language toggle for when only 2 languages are enabled.
Shows both flags side by side.
Parameters:
- current_language: Current language code
- enabled_languages: List of exactly 2 language codes
#}
{% macro language_toggle(current_language='fr', enabled_languages=['fr', 'de']) %}
{% set langs = enabled_languages[:2] if enabled_languages else ['fr', 'de'] %}
{% set current = current_language if current_language in langs else langs[0] %}
{# Uses languageSelector() function per LANG-002 architecture rule #}
<div
x-data="languageSelector('{{ current }}', {{ langs | tojson | safe }})"
class="inline-flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-700 rounded-lg"
>
<template x-for="lang in languages" :key="lang">
<button
@click="setLanguage(lang)"
type="button"
class="p-1.5 rounded transition-all"
:class="currentLang === lang
? 'bg-white dark:bg-gray-600 shadow-sm'
: 'hover:bg-gray-200 dark:hover:bg-gray-600'"
:title="lang.toUpperCase()"
>
<span class="fi" :class="'fi-' + languageFlags[lang]"></span>
</button>
</template>
</div>
{% endmacro %}
{#
Language Settings Form
======================
A form for vendor/admin settings page to configure language preferences.
Parameters:
- current_settings: Dict with current vendor language settings
- form_id: Form ID for submission
#}
{% macro language_settings_form(current_settings=none, form_id='language-settings-form') %}
{% set settings = current_settings or {} %}
{# Native language names per LANG-005 #}
{% set all_languages = [
{'code': 'en', 'name': 'English'},
{'code': 'fr', 'name': 'Français'},
{'code': 'de', 'name': 'Deutsch'},
{'code': 'lb', 'name': 'Lëtzebuergesch'}
] %}
<div
x-data="{
defaultLanguage: '{{ settings.get('default_language', 'fr') }}',
dashboardLanguage: '{{ settings.get('dashboard_language', 'fr') }}',
storefrontLanguage: '{{ settings.get('storefront_language', 'fr') }}',
storefrontLanguages: {{ settings.get('storefront_languages', ['fr', 'de', 'en']) | tojson }},
allLanguages: {{ all_languages | tojson }},
toggleStorefrontLanguage(code) {
const index = this.storefrontLanguages.indexOf(code);
if (index > -1) {
// Don't allow removing if it's the only one or if it's the default
if (this.storefrontLanguages.length > 1 && code !== this.storefrontLanguage) {
this.storefrontLanguages.splice(index, 1);
}
} else {
this.storefrontLanguages.push(code);
}
},
isStorefrontLanguageEnabled(code) {
return this.storefrontLanguages.includes(code);
}
}"
class="space-y-6"
>
{# Default Content Language #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Default Content Language
</label>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
Language used as fallback when content is not available in the requested language.
</p>
<select
x-model="defaultLanguage"
name="default_language"
class="w-full px-4 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<template x-for="lang in allLanguages" :key="lang.code">
<option :value="lang.code" x-text="lang.name" :selected="lang.code === defaultLanguage"></option>
</template>
</select>
</div>
{# Dashboard Language #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Dashboard Language
</label>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
Default language for your vendor dashboard (team members can override in their profile).
</p>
<select
x-model="dashboardLanguage"
name="dashboard_language"
class="w-full px-4 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<template x-for="lang in allLanguages" :key="lang.code">
<option :value="lang.code" x-text="lang.name" :selected="lang.code === dashboardLanguage"></option>
</template>
</select>
</div>
{# Storefront Default Language #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Storefront Default Language
</label>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
Default language shown to customers when they first visit your store.
</p>
<select
x-model="storefrontLanguage"
name="storefront_language"
class="w-full px-4 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<template x-for="lang in allLanguages.filter(l => storefrontLanguages.includes(l.code))" :key="lang.code">
<option :value="lang.code" x-text="lang.name" :selected="lang.code === storefrontLanguage"></option>
</template>
</select>
</div>
{# Enabled Storefront Languages #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Enabled Storefront Languages
</label>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
Languages available to customers in the language selector.
</p>
<div class="space-y-2">
<template x-for="lang in allLanguages" :key="lang.code">
<label class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<input
type="checkbox"
:value="lang.code"
:checked="isStorefrontLanguageEnabled(lang.code)"
@change="toggleStorefrontLanguage(lang.code)"
:disabled="lang.code === storefrontLanguage"
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 disabled:opacity-50"
>
<span class="fi" :class="'fi-' + (lang.code === 'en' ? 'gb' : lang.code === 'lb' ? 'lu' : lang.code)"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="lang.name"></span>
<span x-show="lang.code === storefrontLanguage" class="text-xs text-purple-600 dark:text-purple-400">(default)</span>
</label>
</template>
</div>
<input type="hidden" name="storefront_languages" :value="JSON.stringify(storefrontLanguages)">
</div>
</div>
{% endmacro %}

View File

@@ -40,6 +40,9 @@
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
{# Flag Icons for Language Selector #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css" />
{# Base Shop Styles #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/shop.css') }}">
@@ -130,6 +133,49 @@
</svg>
</button>
{# Language Selector #}
{% set enabled_langs = vendor.storefront_languages if vendor and vendor.storefront_languages else ['fr', 'de', 'en'] %}
{% if enabled_langs|length > 1 %}
<div class="relative" x-data="languageSelector('{{ request.state.language|default("fr") }}', {{ enabled_langs|tojson|safe }})">
<button
@click="isLangOpen = !isLangOpen"
@click.outside="isLangOpen = false"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
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-40 mt-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 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-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
>
<span class="fi" :class="'fi-' + languageFlags[lang]"></span>
<span x-text="languageNames[lang]"></span>
<svg x-show="currentLang === lang" class="w-4 h-4 ml-auto" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</button>
</template>
</div>
</div>
{% endif %}
{# Account #}
<a href="{{ base_url }}shop/account" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -13,6 +13,9 @@
<!-- Tailwind CSS v4 (built locally via standalone CLI) -->
<link rel="stylesheet" href="{{ url_for('static', path='vendor/css/tailwind.output.css') }}" />
<!-- Flag Icons for Language Selector -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css" />
<!-- Alpine Cloak -->
<style>
[x-cloak] { display: none !important; }

View File

@@ -79,6 +79,16 @@
<span x-show="orders.length > 0" class="ml-2 px-2 py-0.5 text-xs bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-300 rounded-full" x-text="totalOrders"></span>
</span>
</button>
<button
@click="activeTab = 'export'"
:class="activeTab === 'export' ? 'border-purple-600 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'"
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors"
>
<span class="flex items-center">
<span x-html="$icon('upload', 'w-4 h-4 mr-2')"></span>
Export
</span>
</button>
<button
@click="activeTab = 'settings'"
:class="activeTab === 'settings' ? 'border-purple-600 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'"
@@ -294,6 +304,149 @@
</div>
</div>
<!-- Export Tab -->
<div x-show="activeTab === 'export'" x-transition>
<div class="grid gap-6 md:grid-cols-2">
<!-- Export Card -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-6">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Export Products to Letzshop
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
Generate a Letzshop-compatible CSV file from your product catalog.
The file uses Google Shopping feed format and includes all required fields.
</p>
<!-- Language Selection -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Export Language
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
Select the language for product titles and descriptions
</p>
<div class="flex flex-wrap gap-3">
<button
@click="exportLanguage = 'fr'"
:class="exportLanguage === 'fr'
? 'bg-purple-100 dark:bg-purple-900/30 border-purple-500 text-purple-700 dark:text-purple-300'
: 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
class="flex items-center gap-2 px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
<span class="fi fi-fr"></span>
Francais
</button>
<button
@click="exportLanguage = 'de'"
:class="exportLanguage === 'de'
? 'bg-purple-100 dark:bg-purple-900/30 border-purple-500 text-purple-700 dark:text-purple-300'
: 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
class="flex items-center gap-2 px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
<span class="fi fi-de"></span>
Deutsch
</button>
<button
@click="exportLanguage = 'en'"
:class="exportLanguage === 'en'
? 'bg-purple-100 dark:bg-purple-900/30 border-purple-500 text-purple-700 dark:text-purple-300'
: 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
class="flex items-center gap-2 px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
<span class="fi fi-gb"></span>
English
</button>
</div>
</div>
<!-- Include Inactive -->
<div class="mb-6">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
x-model="exportIncludeInactive"
class="form-checkbox h-5 w-5 text-purple-600 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-purple-500"
/>
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-400">Include inactive products</span>
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8">
Export products that are currently marked as inactive
</p>
</div>
<!-- Download Button -->
<div class="flex flex-wrap gap-3">
<button
@click="downloadExport()"
:disabled="exporting"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
>
<span x-show="!exporting" x-html="$icon('download', 'w-4 h-4 mr-2')"></span>
<span x-show="exporting" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="exporting ? 'Generating...' : 'Download CSV'"></span>
</button>
</div>
</div>
</div>
<!-- Export Info Card -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-6">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
CSV Format Information
</h3>
<div class="space-y-4 text-sm text-gray-600 dark:text-gray-400">
<div>
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">File Format</h4>
<ul class="list-disc list-inside space-y-1">
<li>Tab-separated values (TSV)</li>
<li>UTF-8 encoding</li>
<li>Google Shopping compatible</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">Included Fields</h4>
<div class="flex flex-wrap gap-2">
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">id</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">title</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">description</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">price</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">image_link</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">availability</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">brand</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">gtin</span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded">+30 more</span>
</div>
</div>
<div>
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">How to Upload</h4>
<ol class="list-decimal list-inside space-y-1">
<li>Download the CSV file</li>
<li>Log in to your Letzshop merchant portal</li>
<li>Navigate to Products > Import</li>
<li>Upload the CSV file</li>
</ol>
</div>
</div>
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div class="flex">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-500 mr-2 flex-shrink-0')"></span>
<div class="text-sm text-blue-700 dark:text-blue-300">
<p class="font-medium">Translation Fallback</p>
<p class="mt-1">If a product doesn't have a translation in the selected language, the system will use English, then fall back to any available translation.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Settings Tab -->
<div x-show="activeTab === 'settings'" x-transition>
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">

View File

@@ -36,6 +36,70 @@
</button>
</li>
<!-- Language selector -->
<li class="relative" x-data="{
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/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
@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>
<!-- Notifications menu -->
<li class="relative">
<button class="relative align-middle rounded-md focus:outline-none focus:shadow-outline-purple"