fix(storefront-i18n): dashboard widgets translate + correct customer-module key paths
Some checks failed
CI / ruff (push) Successful in 17s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Two bugs from Test 5.1 on FR storefront dashboard:

1. Loyalty + Orders dashboard cards (`StorefrontDashboardCard.title`/
   `subtitle`/`value_label`) were hardcoded English. Added `language`
   to `WidgetContext`; customer dashboard route passes
   `request.state.language` through; loyalty and orders widget
   providers now call `translate(..., context.language)` with new
   `widget.*` i18n keys × 4 locales each.

2. Customer-module locale JSON has redundant top-level `customers`
   wrapper, so after the module-locale loader auto-namespaces under
   module code `customers`, the actual key path is
   `customers.customers.customer_number` (matches the existing
   `loyalty.loyalty.wallet.apple` pattern). My earlier sweep used the
   single-prefix path for 8 references — fixed all to double-prefix.

Both bugs were visible end-of-day yesterday after the api container
recreate landed `1bade6e6`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 20:45:46 +02:00
parent acbe2eff1a
commit 5f359283bc
15 changed files with 93 additions and 15 deletions

View File

@@ -72,12 +72,14 @@ class WidgetContext:
date_to: End of date range filter date_to: End of date range filter
limit: Maximum number of items for list widgets limit: Maximum number of items for list widgets
include_details: Whether to include extra details (may be expensive) include_details: Whether to include extra details (may be expensive)
language: Storefront/dashboard locale (e.g. "fr") for i18n of card strings
""" """
date_from: datetime | None = None date_from: datetime | None = None
date_to: datetime | None = None date_to: datetime | None = None
limit: int = 5 limit: int = 5
include_details: bool = False include_details: bool = False
language: str | None = None
# ============================================================================= # =============================================================================

View File

@@ -196,6 +196,7 @@ async def shop_account_dashboard_page(
) )
# Collect dashboard cards from enabled modules via widget protocol # Collect dashboard cards from enabled modules via widget protocol
from app.modules.contracts.widgets import WidgetContext
from app.modules.core.services.widget_aggregator import widget_aggregator from app.modules.core.services.widget_aggregator import widget_aggregator
store = getattr(request.state, "store", None) store = getattr(request.state, "store", None)
@@ -207,6 +208,9 @@ async def shop_account_dashboard_page(
store_id=store.id, store_id=store.id,
customer_id=current_customer.id, customer_id=current_customer.id,
platform_id=platform.id, platform_id=platform.id,
context=WidgetContext(
language=getattr(request.state, "language", None)
),
) )
return templates.TemplateResponse( return templates.TemplateResponse(

View File

@@ -156,12 +156,12 @@
<!-- Name Row --> <!-- Name Row -->
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.first_name') }} *</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.customers.first_name') }} *</label>
<input type="text" x-model="addressForm.first_name" required <input type="text" x-model="addressForm.first_name" required
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.last_name') }} *</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.customers.last_name') }} *</label>
<input type="text" x-model="addressForm.last_name" required <input type="text" x-model="addressForm.last_name" required
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
</div> </div>

View File

@@ -105,7 +105,7 @@
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p> <p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p>
</div> </div>
<div> <div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">{{ _('customers.customer_number') }}</p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">{{ _('customers.customers.customer_number') }}</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p> <p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
</div> </div>
</div> </div>

View File

@@ -66,7 +66,7 @@
<!-- First Name --> <!-- First Name -->
<div> <div>
<label for="first_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label for="first_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ _('customers.first_name') }} <span class="text-red-500">*</span> {{ _('customers.customers.first_name') }} <span class="text-red-500">*</span>
</label> </label>
<input type="text" id="first_name" x-model="profileForm.first_name" required <input type="text" id="first_name" x-model="profileForm.first_name" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
@@ -78,7 +78,7 @@
<!-- Last Name --> <!-- Last Name -->
<div> <div>
<label for="last_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label for="last_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ _('customers.last_name') }} <span class="text-red-500">*</span> {{ _('customers.customers.last_name') }} <span class="text-red-500">*</span>
</label> </label>
<input type="text" id="last_name" x-model="profileForm.last_name" required <input type="text" id="last_name" x-model="profileForm.last_name" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
@@ -263,7 +263,7 @@
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">{{ _('customers.storefront.pages.profile.account_info') }}</h3> <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">{{ _('customers.storefront.pages.profile.account_info') }}</h3>
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"> <dl class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.customer_number') }}</dt> <dt class="text-gray-500 dark:text-gray-400">{{ _('customers.customers.customer_number') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.customer_number || '-'"></dd> <dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.customer_number || '-'"></dd>
</div> </div>
<div> <div>
@@ -271,11 +271,11 @@
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatDate(profile?.created_at)"></dd> <dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatDate(profile?.created_at)"></dd>
</div> </div>
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.total_orders') }}</dt> <dt class="text-gray-500 dark:text-gray-400">{{ _('customers.customers.total_orders') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.total_orders || 0"></dd> <dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.total_orders || 0"></dd>
</div> </div>
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.total_spent') }}</dt> <dt class="text-gray-500 dark:text-gray-400">{{ _('customers.customers.total_spent') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatPrice(profile?.total_spent)"></dd> <dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatPrice(profile?.total_spent)"></dd>
</div> </div>
</dl> </dl>

View File

@@ -1,4 +1,12 @@
{ {
"widget": {
"rewards": {
"title": "Treueprogramm",
"subtitle_member": "Punkte und Prämien anzeigen",
"subtitle_join": "Unserem Treueprogramm beitreten",
"value_label": "Punktestand"
}
},
"loyalty": { "loyalty": {
"module": { "module": {
"name": "Treueprogramme", "name": "Treueprogramme",

View File

@@ -1,4 +1,12 @@
{ {
"widget": {
"rewards": {
"title": "Loyalty Rewards",
"subtitle_member": "View your points & rewards",
"subtitle_join": "Join our rewards program",
"value_label": "Points Balance"
}
},
"loyalty": { "loyalty": {
"module": { "module": {
"name": "Loyalty Programs", "name": "Loyalty Programs",

View File

@@ -1,4 +1,12 @@
{ {
"widget": {
"rewards": {
"title": "Programme de fidélité",
"subtitle_member": "Voir vos points et récompenses",
"subtitle_join": "Rejoindre notre programme de fidélité",
"value_label": "Solde de points"
}
},
"loyalty": { "loyalty": {
"module": { "module": {
"name": "Programmes de Fidélité", "name": "Programmes de Fidélité",

View File

@@ -1,4 +1,12 @@
{ {
"widget": {
"rewards": {
"title": "Treieprogramm",
"subtitle_member": "Är Punkten a Belounungen kucken",
"subtitle_join": "Eisem Treieprogramm bäitrieden",
"value_label": "Punktenzuel"
}
},
"loyalty": { "loyalty": {
"module": { "module": {
"name": "Treieprogrammer", "name": "Treieprogrammer",

View File

@@ -51,6 +51,7 @@ class LoyaltyWidgetProvider:
) -> list[StorefrontDashboardCard]: ) -> list[StorefrontDashboardCard]:
"""Provide the Loyalty Rewards card for the customer dashboard.""" """Provide the Loyalty Rewards card for the customer dashboard."""
from app.modules.loyalty.models.loyalty_card import LoyaltyCard from app.modules.loyalty.models.loyalty_card import LoyaltyCard
from app.utils.i18n import translate
card = ( card = (
db.query(LoyaltyCard) db.query(LoyaltyCard)
@@ -62,17 +63,26 @@ class LoyaltyWidgetProvider:
) )
points = card.points_balance if card else None points = card.points_balance if card else None
subtitle = "View your points & rewards" if card else "Join our rewards program" lang = context.language if context else None
subtitle_key = (
"loyalty.widget.rewards.subtitle_member"
if card
else "loyalty.widget.rewards.subtitle_join"
)
return [ return [
StorefrontDashboardCard( StorefrontDashboardCard(
key="loyalty.rewards", key="loyalty.rewards",
icon="gift", icon="gift",
title="Loyalty Rewards", title=translate("loyalty.widget.rewards.title", lang),
subtitle=subtitle, subtitle=translate(subtitle_key, lang),
route="account/loyalty", route="account/loyalty",
value=points, value=points,
value_label="Points Balance" if points is not None else None, value_label=(
translate("loyalty.widget.rewards.value_label", lang)
if points is not None
else None
),
order=30, order=30,
), ),
] ]

View File

@@ -1,4 +1,11 @@
{ {
"widget": {
"summary": {
"title": "Bestellungen",
"subtitle": "Bestellverlauf anzeigen",
"value_label": "Bestellungen gesamt"
}
},
"orders": { "orders": {
"title": "Bestellungen", "title": "Bestellungen",
"order": "Bestellung", "order": "Bestellung",

View File

@@ -1,4 +1,11 @@
{ {
"widget": {
"summary": {
"title": "Orders",
"subtitle": "View order history",
"value_label": "Total Orders"
}
},
"orders": { "orders": {
"title": "Orders", "title": "Orders",
"order": "Order", "order": "Order",

View File

@@ -1,4 +1,11 @@
{ {
"widget": {
"summary": {
"title": "Commandes",
"subtitle": "Voir l'historique des commandes",
"value_label": "Total des commandes"
}
},
"orders": { "orders": {
"title": "Commandes", "title": "Commandes",
"order": "Commande", "order": "Commande",

View File

@@ -1,4 +1,11 @@
{ {
"widget": {
"summary": {
"title": "Bestellungen",
"subtitle": "Bestellhistorik kucken",
"value_label": "Bestellunge gesamt"
}
},
"orders": { "orders": {
"title": "Bestellungen", "title": "Bestellungen",
"order": "Bestellung", "order": "Bestellung",

View File

@@ -51,6 +51,7 @@ class OrderWidgetProvider:
) -> list[StorefrontDashboardCard]: ) -> list[StorefrontDashboardCard]:
"""Provide the Orders card for the customer dashboard.""" """Provide the Orders card for the customer dashboard."""
from app.modules.orders.models.customer_order_stats import CustomerOrderStats from app.modules.orders.models.customer_order_stats import CustomerOrderStats
from app.utils.i18n import translate
stats = ( stats = (
db.query(CustomerOrderStats) db.query(CustomerOrderStats)
@@ -62,16 +63,17 @@ class OrderWidgetProvider:
) )
total_orders = stats.total_orders if stats else 0 total_orders = stats.total_orders if stats else 0
lang = context.language if context else None
return [ return [
StorefrontDashboardCard( StorefrontDashboardCard(
key="orders.summary", key="orders.summary",
icon="shopping-bag", icon="shopping-bag",
title="Orders", title=translate("orders.widget.summary.title", lang),
subtitle="View order history", subtitle=translate("orders.widget.summary.subtitle", lang),
route="account/orders", route="account/orders",
value=total_orders, value=total_orders,
value_label="Total Orders", value_label=translate("orders.widget.summary.value_label", lang),
order=10, order=10,
), ),
] ]