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 @@
{% endif %}
{{ _('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) }} 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