feat: add configurable currency locale and fix vendor JS init

Currency Locale Configuration:
- Add platform-level storefront settings (locale, currency)
- Create PlatformSettingsService with resolution chain:
  vendor → AdminSetting → environment → hardcoded fallback
- Add storefront_locale nullable field to Vendor model
- Update shop routes to resolve and pass locale to templates
- Add window.SHOP_CONFIG for frontend JavaScript access
- Centralize formatPrice() in shop-layout.js using SHOP_CONFIG
- Remove local formatPrice functions from shop templates

Vendor JS Bug Fix:
- Fix vendorCode being null on all vendor pages
- Root cause: page components overriding init() without calling parent
- Add parent init call to 14 vendor JS files
- Add JS-013 architecture rule to prevent future regressions
- Validator now checks vendor JS files for parent init pattern

Files changed:
- New: app/services/platform_settings_service.py
- New: alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py
- Modified: 14 vendor JS files, shop templates, validation scripts

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-02 21:26:12 +01:00
parent d9d34ab102
commit c87bdfa129
30 changed files with 522 additions and 48 deletions

View File

@@ -462,13 +462,7 @@ function shopOrderDetailPage() {
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
},
formatPrice(amount) {
if (!amount && amount !== 0) return '-';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
// formatPrice is inherited from shopLayoutData() via spread operator
formatDateTime(dateStr) {
if (!dateStr) return '-';

View File

@@ -217,13 +217,7 @@ function shopOrdersPage() {
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
},
formatPrice(amount) {
if (!amount && amount !== 0) return '-';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
// formatPrice is inherited from shopLayoutData() via spread operator
formatDate(dateStr) {
if (!dateStr) return '-';

View File

@@ -523,13 +523,7 @@ function shopProfilePage() {
}
},
formatPrice(amount) {
if (!amount && amount !== 0) return '-';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
// formatPrice is inherited from shopLayoutData() via spread operator
formatDate(dateStr) {
if (!dateStr) return '-';

View File

@@ -308,19 +308,28 @@
{# 1. Log Configuration (must load first) #}
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
{# 2. Icon System #}
{# 2. Global Shop Configuration (currency/locale settings) #}
<script>
window.SHOP_CONFIG = {
locale: '{{ storefront_locale | default("fr-LU") }}',
currency: '{{ storefront_currency | default("EUR") }}',
language: '{{ request.state.language|default("fr") }}'
};
</script>
{# 3. Icon System #}
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
{# 3. Base Shop Layout (Alpine.js component - must load before Alpine) #}
{# 4. Base Shop Layout (Alpine.js component - must load before Alpine) #}
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
{# 4. Utilities #}
{# 5. Utilities #}
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
{# 5. API Client #}
{# 6. API Client #}
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
{# 6. Alpine.js with CDN fallback (deferred - loads last) #}
{# 7. Alpine.js with CDN fallback (deferred - loads last) #}
<script>
(function() {
var script = document.createElement('script');
@@ -339,7 +348,7 @@
})();
</script>
{# 7. Page-specific JavaScript #}
{# 8. Page-specific JavaScript #}
{% block extra_scripts %}{% endblock %}
{# Toast notification container #}

View File

@@ -207,13 +207,7 @@ document.addEventListener('alpine:init', () => {
this.loadProducts();
},
formatPrice(amount) {
if (!amount && amount !== 0) return '';
return new Intl.NumberFormat('fr-LU', {
style: 'currency',
currency: 'EUR'
}).format(amount);
},
// formatPrice is inherited from shopLayoutData() via spread operator
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);