From a28d5d1de566ccb0c454b545d25944591b701e3e Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 14 Mar 2026 17:00:42 +0100 Subject: [PATCH] fix(i18n): convert remaining $t() to server-side _() and fix store dashboard language - Convert storefront enrollment $t() calls to server-side _() to silence dev-toolbar warnings (welcome bonus + join button) - Fix store base template I18n.init() to use current_language (from middleware) instead of dashboard_language (hardcoded store config) so language changes take effect immediately - Switch admin loyalty routes to use get_admin_context() for proper i18n support - Switch store loyalty routes to use core get_store_context() from page_context - Pass program object to storefront enrollment context for server-side rendering - Add LANG-011 architecture rule: enforce $t()/_() over I18n.t() in templates - Fix duplicate file_pattern key in LANG-004 rule (YAML validation error) Co-Authored-By: Claude Opus 4.6 --- .architecture-rules/language.yaml | 31 ++++++++++-- app/modules/loyalty/routes/pages/admin.py | 29 +++-------- app/modules/loyalty/routes/pages/store.py | 49 +------------------ .../loyalty/routes/pages/storefront.py | 6 ++- .../templates/loyalty/storefront/enroll.html | 6 +-- app/templates/store/base.html | 2 +- scripts/validate/validate_architecture.py | 13 +++++ 7 files changed, 56 insertions(+), 80 deletions(-) diff --git a/.architecture-rules/language.yaml b/.architecture-rules/language.yaml index f7981fff..12db33f1 100644 --- a/.architecture-rules/language.yaml +++ b/.architecture-rules/language.yaml @@ -111,11 +111,9 @@ language_rules: function languageSelector(currentLang, enabledLanguages) { ... } window.languageSelector = languageSelector; pattern: - file_pattern: "static/shop/js/shop-layout.js" - required_patterns: - - "function languageSelector" - - "window.languageSelector" - file_pattern: "static/vendor/js/init-alpine.js" + file_patterns: + - "static/shop/js/shop-layout.js" + - "static/vendor/js/init-alpine.js" required_patterns: - "function languageSelector" - "window.languageSelector" @@ -247,3 +245,26 @@ language_rules: pattern: file_pattern: "static/locales/*.json" check: "valid_json" + + - id: "LANG-011" + name: "Use $t() not I18n.t() in HTML templates" + severity: "error" + description: | + In HTML templates, never use I18n.t() directly. It evaluates once + and does NOT re-evaluate when translations finish loading async. + + WRONG (non-reactive, shows raw key then updates): + + + RIGHT (reactive, updates when translations load): + + + BEST (server-side, zero flash): + {{ _('module.key') }} + + Note: I18n.t() is fine in .js files where it's called inside + async callbacks after I18n.init() has completed. + pattern: + file_pattern: "**/*.html" + anti_patterns: + - "I18n.t(" diff --git a/app/modules/loyalty/routes/pages/admin.py b/app/modules/loyalty/routes/pages/admin.py index 1d556457..9d28f840 100644 --- a/app/modules/loyalty/routes/pages/admin.py +++ b/app/modules/loyalty/routes/pages/admin.py @@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access +from app.modules.core.utils.page_context import get_admin_context from app.modules.enums import FrontendType from app.modules.tenancy.models import User from app.templates_config import templates @@ -43,10 +44,7 @@ async def admin_loyalty_programs( """ return templates.TemplateResponse( "loyalty/admin/programs.html", - { - "request": request, - "user": current_user, - }, + get_admin_context(request, db, current_user), ) @@ -62,10 +60,7 @@ async def admin_loyalty_analytics( """ return templates.TemplateResponse( "loyalty/admin/analytics.html", - { - "request": request, - "user": current_user, - }, + get_admin_context(request, db, current_user), ) @@ -91,11 +86,7 @@ async def admin_loyalty_merchant_detail( """ return templates.TemplateResponse( "loyalty/admin/merchant-detail.html", - { - "request": request, - "user": current_user, - "merchant_id": merchant_id, - }, + get_admin_context(request, db, current_user, merchant_id=merchant_id), ) @@ -116,11 +107,7 @@ async def admin_loyalty_program_edit( """ return templates.TemplateResponse( "loyalty/admin/program-edit.html", - { - "request": request, - "user": current_user, - "merchant_id": merchant_id, - }, + get_admin_context(request, db, current_user, merchant_id=merchant_id), ) @@ -141,9 +128,5 @@ async def admin_loyalty_merchant_settings( """ return templates.TemplateResponse( "loyalty/admin/merchant-settings.html", - { - "request": request, - "user": current_user, - "merchant_id": merchant_id, - }, + get_admin_context(request, db, current_user, merchant_id=merchant_id), ) diff --git a/app/modules/loyalty/routes/pages/store.py b/app/modules/loyalty/routes/pages/store.py index 62a3f0f1..0010e7b2 100644 --- a/app/modules/loyalty/routes/pages/store.py +++ b/app/modules/loyalty/routes/pages/store.py @@ -24,10 +24,8 @@ from app.api.deps import ( get_db, get_resolved_store_code, ) -from app.modules.core.services.platform_settings_service import ( - platform_settings_service, -) -from app.modules.tenancy.models import Store, User +from app.modules.core.utils.page_context import get_store_context +from app.modules.tenancy.models import User from app.templates_config import templates logger = logging.getLogger(__name__) @@ -42,49 +40,6 @@ ROUTE_CONFIG = { } -# ============================================================================ -# HELPER: Build Store Context -# ============================================================================ - - -def get_store_context( - request: Request, - db: Session, - current_user: User, - store_code: str, - **extra_context, -) -> dict: - """Build template context for store loyalty pages.""" - # Load store from database - store = db.query(Store).filter(Store.subdomain == store_code).first() - - # Get platform defaults - platform_config = platform_settings_service.get_storefront_config(db) - - # Resolve with store override - storefront_locale = platform_config["locale"] - storefront_currency = platform_config["currency"] - - if store and store.storefront_locale: - storefront_locale = store.storefront_locale - - context = { - "request": request, - "user": current_user, - "store": store, - "store_code": store_code, - "storefront_locale": storefront_locale, - "storefront_currency": storefront_currency, - "dashboard_language": store.dashboard_language if store else "en", - } - - # Add any extra context - if extra_context: - context.update(extra_context) - - return context - - # ============================================================================ # LOYALTY ROOT (Redirect to Terminal) # ============================================================================ diff --git a/app/modules/loyalty/routes/pages/storefront.py b/app/modules/loyalty/routes/pages/storefront.py index 5d628e0b..2327c672 100644 --- a/app/modules/loyalty/routes/pages/storefront.py +++ b/app/modules/loyalty/routes/pages/storefront.py @@ -17,6 +17,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_customer_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_storefront_context from app.modules.customers.models import Customer +from app.modules.loyalty.services import program_service from app.templates_config import templates logger = logging.getLogger(__name__) @@ -114,7 +115,10 @@ async def loyalty_self_enrollment( }, ) - context = get_storefront_context(request, db=db) + store = request.state.store + program = program_service.get_active_program_by_store(db, store.id) if store else None + + context = get_storefront_context(request, db=db, program=program) return templates.TemplateResponse( "loyalty/storefront/enroll.html", context, diff --git a/app/modules/loyalty/templates/loyalty/storefront/enroll.html b/app/modules/loyalty/templates/loyalty/storefront/enroll.html index a62179a5..2673239b 100644 --- a/app/modules/loyalty/templates/loyalty/storefront/enroll.html +++ b/app/modules/loyalty/templates/loyalty/storefront/enroll.html @@ -16,7 +16,7 @@ {{ store.name }} {% endif %}

{{ _('loyalty.enrollment.title') }}

-

+

{{ _('loyalty.enrollment.subtitle', points=program.points_per_euro if program else 1) }}

@@ -38,7 +38,7 @@ class="p-4 text-center text-white" :style="'background-color: ' + (program?.card_color || 'var(--color-primary)')"> - + {{ _('loyalty.enrollment.welcome_bonus', points=program.welcome_bonus_points if program else 0) }}
@@ -119,7 +119,7 @@ class="w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors disabled:opacity-50" :style="'background-color: ' + (program?.card_color || 'var(--color-primary)')"> - +
diff --git a/app/templates/store/base.html b/app/templates/store/base.html index 08058b35..39157bbd 100644 --- a/app/templates/store/base.html +++ b/app/templates/store/base.html @@ -84,7 +84,7 @@ // Wrapped in DOMContentLoaded so deferred i18n.js has loaded document.addEventListener('DOMContentLoaded', async function() { const modules = {% block i18n_modules %}[]{% endblock %}; - await I18n.init('{{ dashboard_language | default("en") }}', modules); + await I18n.init('{{ current_language | default("en") }}', modules); }); diff --git a/scripts/validate/validate_architecture.py b/scripts/validate/validate_architecture.py index 7cbf44b7..ca838de1 100755 --- a/scripts/validate/validate_architecture.py +++ b/scripts/validate/validate_architecture.py @@ -3758,6 +3758,19 @@ class ArchitectureValidator: context=line.strip()[:80], suggestion="Use single quotes: x-data='func({{ data|tojson }})'", ) + + # LANG-011: I18n.t() in templates (should use $t() or server-side _()) + if "I18n.t(" in line: + self._add_violation( + rule_id="LANG-011", + rule_name="Use $t() not I18n.t() in HTML templates", + severity=Severity.ERROR, + file_path=file_path, + line_number=i, + message="I18n.t() is non-reactive in templates. Use $t() (reactive) or _() (server-side)", + context=line.strip()[:80], + suggestion="Replace I18n.t('key') with $t('key') or {{ _('key') }}", + ) except Exception: pass # Skip files that can't be read