fix(storefront): i18n sweep + locale-aware reset-password and welcome email
Some checks failed
CI / ruff (push) Successful in 19s
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

Test 5 (storefront password reset + customer dashboard) surfaced five
issues that all traced back to missing i18n plumbing:

- Forgot-password email arrived in EN regardless of storefront locale —
  handler now prefers request.state.language over customer.preferred_language,
  and loyalty self-enrollment backfills preferred_language for new + returning
  customers so future locale-sensitive flows hit the right language without
  being told twice.
- reset-password.html rendered "undefined" icon boxes because $icon magic
  wasn't loaded in the standalone page — replaced with inline SVGs matching
  the forgot-password.html convention.
- reset-password.html was hardcoded English: added lang attr, full _()
  sweep (22 new auth.* keys × 4 locales), language selector, and JS
  validation strings exposed via tojson.
- "Continue shopping" CTA renamed to "Back to Home" (auth.back_to_home,
  4 locales) on login + forgot + reset — loyalty storefronts have no
  catalog to continue to, mirroring the earlier enroll-success rename.
- /account dashboard, profile, addresses were hardcoded English in the
  body (menu was FR because base layout uses _()). New customers.storefront
  .pages.{dashboard,profile,addresses}.* namespace (~80 keys × 4 locales),
  templates updated, Alpine JS strings injected via window.__*I18n.

18 files, 18 changed; arch validation: 126 warnings before = 126 after,
mkdocs --strict clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 23:06:11 +02:00
parent f04cbb8ca2
commit 10a99f98fe
18 changed files with 722 additions and 162 deletions

View File

@@ -59,6 +59,95 @@
"profile": "Profil",
"addresses": "Adressen",
"settings": "Einstellungen"
},
"pages": {
"dashboard": {
"title": "Mein Konto",
"welcome_back": "Willkommen zurück, {name}!",
"profile_card_title": "Profil",
"profile_card_subtitle": "Informationen bearbeiten",
"addresses_card_title": "Adressen",
"addresses_card_subtitle": "Adressen verwalten",
"messages_card_title": "Nachrichten",
"messages_card_subtitle": "Support kontaktieren",
"unread_messages_singular": "{count} ungelesene Nachricht",
"unread_messages_plural": "{count} ungelesene Nachrichten",
"summary_title": "Kontoübersicht",
"customer_since": "Kunde seit",
"logout": "Abmelden",
"logout_confirm_title": "Abmeldung bestätigen",
"logout_confirm_message": "Sind Sie sicher, dass Sie sich abmelden möchten? Sie müssen sich erneut anmelden, um auf Ihr Konto zuzugreifen.",
"logout_success": "Abmeldung erfolgreich",
"logout_failed": "Abmeldung fehlgeschlagen"
},
"profile": {
"breadcrumb_account": "Mein Konto",
"breadcrumb_profile": "Profil",
"title": "Mein Profil",
"subtitle": "Verwalten Sie Ihre Kontoinformationen und Einstellungen",
"info_section_title": "Profilinformationen",
"info_section_subtitle": "Persönliche Daten aktualisieren",
"email_label": "E-Mail-Adresse",
"save_changes": "Änderungen speichern",
"saving": "Speichern...",
"prefs_section_title": "Einstellungen",
"prefs_section_subtitle": "Verwalten Sie Ihre Kontoeinstellungen",
"preferred_language": "Bevorzugte Sprache",
"use_shop_default": "Shop-Standard verwenden",
"marketing_communications": "Marketing-Kommunikation",
"marketing_desc": "E-Mails über neue Produkte, Angebote und Aktionen erhalten",
"save_preferences": "Einstellungen speichern",
"change_password_subtitle": "Aktualisieren Sie Ihr Kontopasswort",
"changing": "Wird geändert...",
"account_info": "Kontoinformationen",
"member_since": "Mitglied seit",
"profile_updated": "Profil erfolgreich aktualisiert",
"preferences_updated": "Einstellungen erfolgreich aktualisiert",
"password_changed": "Passwort erfolgreich geändert",
"failed_to_load": "Profil konnte nicht geladen werden",
"failed_to_save_profile": "Profil konnte nicht gespeichert werden",
"failed_to_save_preferences": "Einstellungen konnten nicht gespeichert werden",
"failed_to_change_password": "Passwort konnte nicht geändert werden"
},
"addresses": {
"title": "Meine Adressen",
"subtitle": "Verwalten Sie Ihre Liefer- und Rechnungsadressen",
"add_address": "Adresse hinzufügen",
"empty_state_title": "Noch keine Adressen",
"empty_state_subtitle": "Fügen Sie Ihre erste Adresse hinzu, um den Bezahlvorgang zu beschleunigen.",
"add_first_address": "Erste Adresse hinzufügen",
"default_shipping": "Standard-Lieferadresse",
"default_billing": "Standard-Rechnungsadresse",
"shipping": "Lieferung",
"billing": "Rechnung",
"set_default": "Als Standard festlegen",
"edit_address": "Adresse bearbeiten",
"add_new_address": "Neue Adresse",
"address_type": "Adresstyp",
"shipping_address": "Lieferadresse",
"billing_address": "Rechnungsadresse",
"company_optional": "Unternehmen (optional)",
"address_line_1": "Adresse",
"address_line_2_optional": "Adresszusatz (optional)",
"postal_code": "Postleitzahl",
"city": "Stadt",
"country": "Land",
"set_as_default_shipping": "Als Standard-Lieferadresse festlegen",
"set_as_default_billing": "Als Standard-Rechnungsadresse festlegen",
"save_changes": "Änderungen speichern",
"saving": "Speichern...",
"delete_address": "Adresse löschen",
"delete_confirm": "Sind Sie sicher, dass Sie diese Adresse löschen möchten? Dieser Vorgang ist nicht rückgängig zu machen.",
"deleting": "Wird gelöscht...",
"address_updated": "Adresse aktualisiert",
"address_added": "Adresse hinzugefügt",
"address_deleted": "Adresse gelöscht",
"default_updated": "Standardadresse aktualisiert",
"failed_to_load": "Adressen konnten nicht geladen werden. Bitte erneut versuchen.",
"failed_to_save": "Adresse konnte nicht gespeichert werden. Bitte erneut versuchen.",
"failed_to_delete": "Adresse konnte nicht gelöscht werden",
"failed_to_set_default": "Standardadresse konnte nicht festgelegt werden"
}
}
}
}

View File

@@ -59,6 +59,95 @@
"profile": "Profile",
"addresses": "Addresses",
"settings": "Settings"
},
"pages": {
"dashboard": {
"title": "My Account",
"welcome_back": "Welcome back, {name}!",
"profile_card_title": "Profile",
"profile_card_subtitle": "Edit your information",
"addresses_card_title": "Addresses",
"addresses_card_subtitle": "Manage addresses",
"messages_card_title": "Messages",
"messages_card_subtitle": "Contact support",
"unread_messages_singular": "{count} unread message",
"unread_messages_plural": "{count} unread messages",
"summary_title": "Account Summary",
"customer_since": "Customer Since",
"logout": "Logout",
"logout_confirm_title": "Logout Confirmation",
"logout_confirm_message": "Are you sure you want to logout? You'll need to sign in again to access your account.",
"logout_success": "Logged out successfully",
"logout_failed": "Logout failed"
},
"profile": {
"breadcrumb_account": "My Account",
"breadcrumb_profile": "Profile",
"title": "My Profile",
"subtitle": "Manage your account information and preferences",
"info_section_title": "Profile Information",
"info_section_subtitle": "Update your personal details",
"email_label": "Email Address",
"save_changes": "Save Changes",
"saving": "Saving...",
"prefs_section_title": "Preferences",
"prefs_section_subtitle": "Manage your account preferences",
"preferred_language": "Preferred Language",
"use_shop_default": "Use shop default",
"marketing_communications": "Marketing Communications",
"marketing_desc": "Receive emails about new products, offers, and promotions",
"save_preferences": "Save Preferences",
"change_password_subtitle": "Update your account password",
"changing": "Changing...",
"account_info": "Account Information",
"member_since": "Member Since",
"profile_updated": "Profile updated successfully",
"preferences_updated": "Preferences updated successfully",
"password_changed": "Password changed successfully",
"failed_to_load": "Failed to load profile",
"failed_to_save_profile": "Failed to save profile",
"failed_to_save_preferences": "Failed to save preferences",
"failed_to_change_password": "Failed to change password"
},
"addresses": {
"title": "My Addresses",
"subtitle": "Manage your shipping and billing addresses",
"add_address": "Add Address",
"empty_state_title": "No addresses yet",
"empty_state_subtitle": "Add your first address to speed up checkout.",
"add_first_address": "Add Your First Address",
"default_shipping": "Default Shipping",
"default_billing": "Default Billing",
"shipping": "Shipping",
"billing": "Billing",
"set_default": "Set as Default",
"edit_address": "Edit Address",
"add_new_address": "Add New Address",
"address_type": "Address Type",
"shipping_address": "Shipping Address",
"billing_address": "Billing Address",
"company_optional": "Company (optional)",
"address_line_1": "Address",
"address_line_2_optional": "Address Line 2 (optional)",
"postal_code": "Postal Code",
"city": "City",
"country": "Country",
"set_as_default_shipping": "Set as default shipping address",
"set_as_default_billing": "Set as default billing address",
"save_changes": "Save Changes",
"saving": "Saving...",
"delete_address": "Delete Address",
"delete_confirm": "Are you sure you want to delete this address? This action cannot be undone.",
"deleting": "Deleting...",
"address_updated": "Address updated",
"address_added": "Address added",
"address_deleted": "Address deleted",
"default_updated": "Default address updated",
"failed_to_load": "Failed to load addresses. Please try again.",
"failed_to_save": "Failed to save address. Please try again.",
"failed_to_delete": "Failed to delete address",
"failed_to_set_default": "Failed to set default address"
}
}
}
}

View File

@@ -59,6 +59,95 @@
"profile": "Profil",
"addresses": "Adresses",
"settings": "Paramètres"
},
"pages": {
"dashboard": {
"title": "Mon compte",
"welcome_back": "Bon retour, {name} !",
"profile_card_title": "Profil",
"profile_card_subtitle": "Modifier vos informations",
"addresses_card_title": "Adresses",
"addresses_card_subtitle": "Gérer vos adresses",
"messages_card_title": "Messages",
"messages_card_subtitle": "Contacter le support",
"unread_messages_singular": "{count} message non lu",
"unread_messages_plural": "{count} messages non lus",
"summary_title": "Résumé du compte",
"customer_since": "Client depuis",
"logout": "Se déconnecter",
"logout_confirm_title": "Confirmer la déconnexion",
"logout_confirm_message": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez vous reconnecter pour accéder à votre compte.",
"logout_success": "Déconnexion réussie",
"logout_failed": "Échec de la déconnexion"
},
"profile": {
"breadcrumb_account": "Mon compte",
"breadcrumb_profile": "Profil",
"title": "Mon profil",
"subtitle": "Gérez les informations et préférences de votre compte",
"info_section_title": "Informations du profil",
"info_section_subtitle": "Mettez à jour vos informations personnelles",
"email_label": "Adresse e-mail",
"save_changes": "Enregistrer",
"saving": "Enregistrement...",
"prefs_section_title": "Préférences",
"prefs_section_subtitle": "Gérez les préférences de votre compte",
"preferred_language": "Langue préférée",
"use_shop_default": "Utiliser la langue de la boutique",
"marketing_communications": "Communications marketing",
"marketing_desc": "Recevoir des e-mails sur les nouveaux produits, offres et promotions",
"save_preferences": "Enregistrer les préférences",
"change_password_subtitle": "Mettez à jour le mot de passe de votre compte",
"changing": "Changement...",
"account_info": "Informations du compte",
"member_since": "Membre depuis",
"profile_updated": "Profil mis à jour avec succès",
"preferences_updated": "Préférences mises à jour avec succès",
"password_changed": "Mot de passe modifié avec succès",
"failed_to_load": "Échec du chargement du profil",
"failed_to_save_profile": "Échec de l'enregistrement du profil",
"failed_to_save_preferences": "Échec de l'enregistrement des préférences",
"failed_to_change_password": "Échec du changement de mot de passe"
},
"addresses": {
"title": "Mes adresses",
"subtitle": "Gérez vos adresses de livraison et de facturation",
"add_address": "Ajouter une adresse",
"empty_state_title": "Aucune adresse",
"empty_state_subtitle": "Ajoutez votre première adresse pour accélérer le paiement.",
"add_first_address": "Ajouter votre première adresse",
"default_shipping": "Livraison par défaut",
"default_billing": "Facturation par défaut",
"shipping": "Livraison",
"billing": "Facturation",
"set_default": "Définir par défaut",
"edit_address": "Modifier l'adresse",
"add_new_address": "Nouvelle adresse",
"address_type": "Type d'adresse",
"shipping_address": "Adresse de livraison",
"billing_address": "Adresse de facturation",
"company_optional": "Société (facultatif)",
"address_line_1": "Adresse",
"address_line_2_optional": "Complément d'adresse (facultatif)",
"postal_code": "Code postal",
"city": "Ville",
"country": "Pays",
"set_as_default_shipping": "Définir comme adresse de livraison par défaut",
"set_as_default_billing": "Définir comme adresse de facturation par défaut",
"save_changes": "Enregistrer",
"saving": "Enregistrement...",
"delete_address": "Supprimer l'adresse",
"delete_confirm": "Êtes-vous sûr de vouloir supprimer cette adresse ? Cette action est irréversible.",
"deleting": "Suppression...",
"address_updated": "Adresse mise à jour",
"address_added": "Adresse ajoutée",
"address_deleted": "Adresse supprimée",
"default_updated": "Adresse par défaut mise à jour",
"failed_to_load": "Échec du chargement des adresses. Veuillez réessayer.",
"failed_to_save": "Échec de l'enregistrement de l'adresse. Veuillez réessayer.",
"failed_to_delete": "Échec de la suppression de l'adresse",
"failed_to_set_default": "Échec de la définition de l'adresse par défaut"
}
}
}
}

View File

@@ -59,6 +59,95 @@
"profile": "Profil",
"addresses": "Adressen",
"settings": "Astellungen"
},
"pages": {
"dashboard": {
"title": "Mäi Kont",
"welcome_back": "Wëllkomm zréck, {name} !",
"profile_card_title": "Profil",
"profile_card_subtitle": "Informatiounen änneren",
"addresses_card_title": "Adressen",
"addresses_card_subtitle": "Adressen verwalten",
"messages_card_title": "Noriichten",
"messages_card_subtitle": "Support kontaktéieren",
"unread_messages_singular": "{count} ongeliesen Noriicht",
"unread_messages_plural": "{count} ongeliesen Noriichten",
"summary_title": "Kontoiwwersiicht",
"customer_since": "Client zënter",
"logout": "Ofmellen",
"logout_confirm_title": "Ofmellen bestätegen",
"logout_confirm_message": "Sidd Dir sécher datt Dir Iech ofmellen wëllt? Dir musst Iech erëm aloggen fir op Äre Kont ze zougräifen.",
"logout_success": "Erfollegräich ofgemellt",
"logout_failed": "Ofmellen feelgeschloen"
},
"profile": {
"breadcrumb_account": "Mäi Kont",
"breadcrumb_profile": "Profil",
"title": "Mäi Profil",
"subtitle": "Verwalt Är Kontodaten an Astellungen",
"info_section_title": "Profilinformatiounen",
"info_section_subtitle": "Är perséinlech Donnéeën updaten",
"email_label": "E-Mail-Adress",
"save_changes": "Änneren späicheren",
"saving": "Späicheren...",
"prefs_section_title": "Astellungen",
"prefs_section_subtitle": "Verwalt Är Kontosastellungen",
"preferred_language": "Bevorzucht Sprooch",
"use_shop_default": "Buttek-Standard benotzen",
"marketing_communications": "Marketing-Kommunikatioun",
"marketing_desc": "E-Maile mat neie Produkter, Offeren a Promotioune kréien",
"save_preferences": "Astellunge späicheren",
"change_password_subtitle": "Aktualiséiert Äert Kontopasswuert",
"changing": "Änneren...",
"account_info": "Kontoinformatiounen",
"member_since": "Member zënter",
"profile_updated": "Profil erfollegräich aktualiséiert",
"preferences_updated": "Astellungen erfollegräich aktualiséiert",
"password_changed": "Passwuert erfollegräich geännert",
"failed_to_load": "Profil konnt net geluede ginn",
"failed_to_save_profile": "Profil konnt net späichert ginn",
"failed_to_save_preferences": "Astellunge konnten net späichert ginn",
"failed_to_change_password": "Passwuert konnt net geännert ginn"
},
"addresses": {
"title": "Meng Adressen",
"subtitle": "Verwalt Är Liwwer- a Rechnungsadressen",
"add_address": "Adress derbäisetzen",
"empty_state_title": "Nach keng Adressen",
"empty_state_subtitle": "Setzt Är éischt Adress derbäi fir den Bezuelvirgang ze beschleunegen.",
"add_first_address": "Éischt Adress derbäisetzen",
"default_shipping": "Standard-Liwweradress",
"default_billing": "Standard-Rechnungsadress",
"shipping": "Liwwerung",
"billing": "Rechnung",
"set_default": "Als Standard festleeën",
"edit_address": "Adress änneren",
"add_new_address": "Nei Adress",
"address_type": "Adresstyp",
"shipping_address": "Liwweradress",
"billing_address": "Rechnungsadress",
"company_optional": "Entreprise (fakultativ)",
"address_line_1": "Adress",
"address_line_2_optional": "Adresszousatz (fakultativ)",
"postal_code": "Postleitzuel",
"city": "Stad",
"country": "Land",
"set_as_default_shipping": "Als Standard-Liwweradress festleeën",
"set_as_default_billing": "Als Standard-Rechnungsadress festleeën",
"save_changes": "Änneren späicheren",
"saving": "Späicheren...",
"delete_address": "Adress läschen",
"delete_confirm": "Sidd Dir sécher datt Dir dës Adress läsche wëllt? Dës Aktioun kann net réckgängeg gemaach ginn.",
"deleting": "Läschen...",
"address_updated": "Adress aktualiséiert",
"address_added": "Adress derbäigesat",
"address_deleted": "Adress geläscht",
"default_updated": "Standardadress aktualiséiert",
"failed_to_load": "Adressen konnten net geluede ginn. Probéiert nach eng Kéier.",
"failed_to_save": "Adress konnt net späichert ginn. Probéiert nach eng Kéier.",
"failed_to_delete": "Adress konnt net geläscht ginn",
"failed_to_set_default": "Standardadress konnt net festgeluecht ginn"
}
}
}
}

View File

@@ -270,11 +270,12 @@ def forgot_password(
reset_link = f"{scheme}://{host}/account/reset-password?token={plaintext_token}"
email_service = EmailService(db)
request_language = getattr(request.state, "language", None)
email_service.send_template(
template_code="password_reset",
to_email=customer.email,
to_name=customer.full_name,
language=customer.preferred_language or "en",
language=request_language or customer.preferred_language or "en",
variables={
"customer_name": customer.first_name or customer.full_name,
"reset_link": reset_link,

View File

@@ -568,6 +568,7 @@ class CustomerService:
last_name: str = "",
phone: str | None = None,
birth_date: date | None = None,
preferred_language: str | None = None,
) -> Customer:
"""
Create a customer for loyalty/external enrollment.
@@ -606,6 +607,7 @@ class CustomerService:
last_name=last_name,
phone=phone,
birth_date=birth_date,
preferred_language=preferred_language,
hashed_password=unusable_hash,
customer_number=cust_number,
store_id=store_id,

View File

@@ -1,7 +1,7 @@
{# app/templates/storefront/account/addresses.html #}
{% extends "storefront/base.html" %}
{% block title %}My Addresses - {{ store.name }}{% endblock %}
{% block title %}{{ _('customers.storefront.pages.addresses.title') }} - {{ store.name }}{% endblock %}
{% block alpine_data %}addressesPage(){% endblock %}
@@ -10,14 +10,14 @@
<!-- Page Header -->
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Addresses</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Manage your shipping and billing addresses</p>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.addresses.title') }}</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.addresses.subtitle') }}</p>
</div>
<button @click="openAddModal()"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
style="background-color: var(--color-primary)">
<span class="-ml-1 mr-2 h-5 w-5" x-html="$icon('plus', 'h-5 w-5')"></span>
Add Address
{{ _('customers.storefront.pages.addresses.add_address') }}
</button>
</div>
@@ -38,12 +38,12 @@
<div x-show="!loading && !error && addresses.length === 0"
class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-12 text-center">
<span class="mx-auto h-12 w-12 text-gray-400 block" x-html="$icon('map-pin', 'h-12 w-12 mx-auto')"></span>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No addresses yet</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Add your first address to speed up checkout.</p>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">{{ _('customers.storefront.pages.addresses.empty_state_title') }}</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.addresses.empty_state_subtitle') }}</p>
<button @click="openAddModal()"
class="mt-6 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark"
style="background-color: var(--color-primary)">
Add Your First Address
{{ _('customers.storefront.pages.addresses.add_first_address') }}
</button>
</div>
@@ -56,14 +56,14 @@
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="address.address_type === 'shipping' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'">
<span class="-ml-0.5 mr-1 h-3 w-3" x-html="$icon('check-circle', 'h-3 w-3')"></span>
Default <span x-text="address.address_type === 'shipping' ? 'Shipping' : 'Billing'" class="ml-1"></span>
<span x-text="address.address_type === 'shipping' ? i18n.defaultShipping : i18n.defaultBilling"></span>
</span>
</div>
<!-- Address Type Badge (non-default) -->
<div x-show="!address.is_default" class="absolute top-4 right-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
x-text="address.address_type === 'shipping' ? 'Shipping' : 'Billing'"></span>
x-text="address.address_type === 'shipping' ? i18n.shipping : i18n.billing"></span>
</div>
<!-- Address Content -->
@@ -81,16 +81,16 @@
<button @click="openEditModal(address)"
class="text-sm font-medium text-primary hover:text-primary-dark"
style="color: var(--color-primary)">
Edit
{{ _('common.edit') }}
</button>
<button x-show="!address.is_default"
@click="setAsDefault(address.id)"
class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
Set as Default
{{ _('customers.storefront.pages.addresses.set_default') }}
</button>
<button @click="openDeleteModal(address.id)"
class="text-sm font-medium text-red-600 hover:text-red-700">
Delete
{{ _('common.delete') }}
</button>
</div>
</div>
@@ -140,28 +140,28 @@
<div class="sm:flex sm:items-start">
<div class="w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-6"
x-text="editingAddress ? 'Edit Address' : 'Add New Address'"></h3>
x-text="editingAddress ? i18n.editAddress : i18n.addNewAddress"></h3>
<form @submit.prevent="saveAddress()" class="space-y-4">
<!-- Address Type -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address Type</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.address_type') }}</label>
<select x-model="addressForm.address_type"
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">
<option value="shipping">Shipping Address</option>
<option value="billing">Billing Address</option>
<option value="shipping">{{ _('customers.storefront.pages.addresses.shipping_address') }}</option>
<option value="billing">{{ _('customers.storefront.pages.addresses.billing_address') }}</option>
</select>
</div>
<!-- Name Row -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">First Name *</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.first_name') }} *</label>
<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">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Last Name *</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.last_name') }} *</label>
<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">
</div>
@@ -169,21 +169,21 @@
<!-- Company -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Company (optional)</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.company_optional') }}</label>
<input type="text" x-model="addressForm.company"
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>
<!-- Address Line 1 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address *</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.address_line_1') }} *</label>
<input type="text" x-model="addressForm.address_line_1" 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">
</div>
<!-- Address Line 2 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address Line 2 (optional)</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.address_line_2_optional') }}</label>
<input type="text" x-model="addressForm.address_line_2"
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>
@@ -191,12 +191,12 @@
<!-- City & Postal Code -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Postal Code *</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.postal_code') }} *</label>
<input type="text" x-model="addressForm.postal_code" 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">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">City *</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.city') }} *</label>
<input type="text" x-model="addressForm.city" 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">
</div>
@@ -204,7 +204,7 @@
<!-- Country -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Country *</label>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.country') }} *</label>
<select x-model="addressForm.country_iso"
@change="addressForm.country_name = countries.find(c => c.iso === addressForm.country_iso)?.name || ''"
required
@@ -220,9 +220,8 @@
<input type="checkbox" x-model="addressForm.is_default"
class="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary"
style="color: var(--color-primary)">
<label class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Set as default <span x-text="addressForm.address_type === 'shipping' ? 'shipping' : 'billing'"></span> address
</label>
<label class="ml-2 text-sm text-gray-700 dark:text-gray-300"
x-text="addressForm.address_type === 'shipping' ? i18n.setAsDefaultShipping : i18n.setAsDefaultBilling"></label>
</div>
<!-- Error Message -->
@@ -234,14 +233,14 @@
<div class="mt-6 flex justify-end space-x-3">
<button type="button" @click="showAddressModal = false"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
Cancel
{{ _('common.cancel') }}
</button>
<button type="submit"
:disabled="saving"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark disabled:opacity-50"
style="background-color: var(--color-primary)">
<span x-show="!saving" x-text="editingAddress ? 'Save Changes' : 'Add Address'"></span>
<span x-show="saving">Saving...</span>
<span x-show="!saving" x-text="editingAddress ? i18n.saveChanges : i18n.addAddress"></span>
<span x-show="saving">{{ _('customers.storefront.pages.addresses.saving') }}</span>
</button>
</div>
</form>
@@ -288,10 +287,10 @@
<span class="h-6 w-6 text-red-600 dark:text-red-400" x-html="$icon('exclamation-triangle', 'h-6 w-6')"></span>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Delete Address</h3>
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">{{ _('customers.storefront.pages.addresses.delete_address') }}</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Are you sure you want to delete this address? This action cannot be undone.
{{ _('customers.storefront.pages.addresses.delete_confirm') }}
</p>
</div>
</div>
@@ -301,12 +300,12 @@
<button @click="confirmDelete()"
:disabled="deleting"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50">
<span x-show="!deleting">Delete</span>
<span x-show="deleting">Deleting...</span>
<span x-show="!deleting">{{ _('common.delete') }}</span>
<span x-show="deleting">{{ _('customers.storefront.pages.addresses.deleting') }}</span>
</button>
<button @click="showDeleteModal = false"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 sm:mt-0 sm:w-auto sm:text-sm">
Cancel
{{ _('common.cancel') }}
</button>
</div>
</div>
@@ -316,9 +315,31 @@
{% block extra_scripts %}
<script>
window.__addressesPageI18n = {
defaultShipping: {{ _('customers.storefront.pages.addresses.default_shipping')|tojson }},
defaultBilling: {{ _('customers.storefront.pages.addresses.default_billing')|tojson }},
shipping: {{ _('customers.storefront.pages.addresses.shipping')|tojson }},
billing: {{ _('customers.storefront.pages.addresses.billing')|tojson }},
editAddress: {{ _('customers.storefront.pages.addresses.edit_address')|tojson }},
addNewAddress: {{ _('customers.storefront.pages.addresses.add_new_address')|tojson }},
addAddress: {{ _('customers.storefront.pages.addresses.add_address')|tojson }},
saveChanges: {{ _('customers.storefront.pages.addresses.save_changes')|tojson }},
setAsDefaultShipping: {{ _('customers.storefront.pages.addresses.set_as_default_shipping')|tojson }},
setAsDefaultBilling: {{ _('customers.storefront.pages.addresses.set_as_default_billing')|tojson }},
addressUpdated: {{ _('customers.storefront.pages.addresses.address_updated')|tojson }},
addressAdded: {{ _('customers.storefront.pages.addresses.address_added')|tojson }},
addressDeleted: {{ _('customers.storefront.pages.addresses.address_deleted')|tojson }},
defaultUpdated: {{ _('customers.storefront.pages.addresses.default_updated')|tojson }},
failedToLoad: {{ _('customers.storefront.pages.addresses.failed_to_load')|tojson }},
failedToSave: {{ _('customers.storefront.pages.addresses.failed_to_save')|tojson }},
failedToDelete: {{ _('customers.storefront.pages.addresses.failed_to_delete')|tojson }},
failedToSetDefault: {{ _('customers.storefront.pages.addresses.failed_to_set_default')|tojson }},
};
function addressesPage() {
const i18n = window.__addressesPageI18n || {};
return {
...storefrontLayoutData(),
i18n,
// State
loading: true,
@@ -403,14 +424,14 @@ function addressesPage() {
window.location.href = '{{ base_url }}account/login';
return;
}
throw new Error('Failed to load addresses');
throw new Error(i18n.failedToLoad);
}
const data = await response.json();
this.addresses = data.addresses;
} catch (err) {
console.error('[ADDRESSES] Error loading:', err);
this.error = 'Failed to load addresses. Please try again.';
this.error = i18n.failedToLoad;
} finally {
this.loading = false;
}
@@ -476,15 +497,15 @@ function addressesPage() {
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || data.message || 'Failed to save address');
throw new Error(data.detail || data.message || i18n.failedToSave);
}
this.showAddressModal = false;
this.showToast(this.editingAddress ? 'Address updated' : 'Address added', 'success');
this.showToast(this.editingAddress ? i18n.addressUpdated : i18n.addressAdded, 'success');
await this.loadAddresses();
} catch (err) {
console.error('[ADDRESSES] Error saving:', err);
this.formError = err.message || 'Failed to save address. Please try again.';
this.formError = err.message || i18n.failedToSave;
} finally {
this.saving = false;
}
@@ -508,15 +529,15 @@ function addressesPage() {
});
if (!response.ok) {
throw new Error('Failed to delete address');
throw new Error(i18n.failedToDelete);
}
this.showDeleteModal = false;
this.showToast('Address deleted', 'success');
this.showToast(i18n.addressDeleted, 'success');
await this.loadAddresses();
} catch (err) {
console.error('[ADDRESSES] Error deleting:', err);
this.showToast('Failed to delete address', 'error');
this.showToast(i18n.failedToDelete, 'error');
} finally {
this.deleting = false;
}
@@ -533,14 +554,14 @@ function addressesPage() {
});
if (!response.ok) {
throw new Error('Failed to set default address');
throw new Error(i18n.failedToSetDefault);
}
this.showToast('Default address updated', 'success');
this.showToast(i18n.defaultUpdated, 'success');
await this.loadAddresses();
} catch (err) {
console.error('[ADDRESSES] Error setting default:', err);
this.showToast('Failed to set default address', 'error');
this.showToast(i18n.failedToSetDefault, 'error');
}
}
}

View File

@@ -2,7 +2,7 @@
{% extends "storefront/base.html" %}
{% from 'shared/macros/modals.html' import confirm_modal %}
{% block title %}My Account - {{ store.name }}{% endblock %}
{% block title %}{{ _('customers.storefront.pages.dashboard.title') }} - {{ store.name }}{% endblock %}
{% block alpine_data %}accountDashboard(){% endblock %}
@@ -10,8 +10,8 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Account</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Welcome back, {{ user.first_name }}!</p>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.title') }}</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.welcome_back', name=user.first_name) }}</p>
</div>
<!-- Dashboard Grid -->
@@ -49,8 +49,8 @@
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('user', 'h-8 w-8')"></span>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Profile</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Edit your information</p>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.profile_card_title') }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.profile_card_subtitle') }}</p>
</div>
</div>
<div>
@@ -66,8 +66,8 @@
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('map-pin', 'h-8 w-8')"></span>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Addresses</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Manage addresses</p>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.addresses_card_title') }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.addresses_card_subtitle') }}</p>
</div>
</div>
</a>
@@ -85,26 +85,27 @@
x-text="unreadCount > 9 ? '9+' : unreadCount"></span>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Messages</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Contact support</p>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.messages_card_title') }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.messages_card_subtitle') }}</p>
</div>
</div>
<div x-show="unreadCount > 0">
<p class="text-sm text-primary font-medium" style="color: var(--color-primary)" x-text="unreadCount + ' unread message' + (unreadCount > 1 ? 's' : '')"></p>
<p class="text-sm text-primary font-medium" style="color: var(--color-primary)"
x-text="(unreadCount === 1 ? {{ _('customers.storefront.pages.dashboard.unread_messages_singular')|tojson }} : {{ _('customers.storefront.pages.dashboard.unread_messages_plural')|tojson }}).replace('{count}', unreadCount)"></p>
</div>
</a>
</div>
<!-- Account Summary -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Account Summary</h3>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">{{ _('customers.storefront.pages.dashboard.summary_title') }}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Since</p>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">{{ _('customers.storefront.pages.dashboard.customer_since') }}</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p>
</div>
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Number</p>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">{{ _('customers.customer_number') }}</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
</div>
</div>
@@ -114,7 +115,7 @@
<div class="mt-8 flex justify-end">
<button @click="showLogoutModal = true"
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Logout
{{ _('customers.storefront.pages.dashboard.logout') }}
</button>
</div>
</div>
@@ -122,19 +123,24 @@
<!-- Logout Confirmation Modal -->
{{ confirm_modal(
id='logoutModal',
title='Logout Confirmation',
message="Are you sure you want to logout? You'll need to sign in again to access your account.",
title=_('customers.storefront.pages.dashboard.logout_confirm_title'),
message=_('customers.storefront.pages.dashboard.logout_confirm_message'),
confirm_action='confirmLogout()',
show_var='showLogoutModal',
confirm_text='Logout',
cancel_text='Cancel',
confirm_text=_('customers.storefront.pages.dashboard.logout'),
cancel_text=_('common.cancel'),
variant='danger'
) }}
{% endblock %}
{% block extra_scripts %}
<script>
window.__accountDashboardI18n = {
logoutSuccess: {{ _('customers.storefront.pages.dashboard.logout_success')|tojson }},
logoutFailed: {{ _('customers.storefront.pages.dashboard.logout_failed')|tojson }},
};
function accountDashboard() {
const i18n = window.__accountDashboardI18n || {};
return {
...storefrontLayoutData(),
showLogoutModal: false,
@@ -155,7 +161,7 @@ function accountDashboard() {
localStorage.removeItem('customer_token');
// Show success message
this.showToast('Logged out successfully', 'success');
this.showToast(i18n.logoutSuccess, 'success');
// Redirect to login page
setTimeout(() => {
@@ -163,7 +169,7 @@ function accountDashboard() {
}, 500);
} else {
console.error('Logout failed with status:', response.status);
this.showToast('Logout failed', 'error');
this.showToast(i18n.logoutFailed, 'error');
// Still redirect on failure (cookie might be deleted)
setTimeout(() => {
window.location.href = '{{ base_url }}account/login';
@@ -172,7 +178,7 @@ function accountDashboard() {
})
.catch(error => {
console.error('Logout error:', error);
this.showToast('Logout failed', 'error');
this.showToast(i18n.logoutFailed, 'error');
// Redirect anyway
setTimeout(() => {
window.location.href = '{{ base_url }}account/login';

View File

@@ -153,7 +153,7 @@
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}">
&larr; {{ _("auth.continue_shopping") }}
&larr; {{ _("auth.back_to_home") }}
</a>
</p>
@@ -180,6 +180,15 @@
<!-- Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
{# Translated client-side strings — kept in sync with auth.* keys above #}
<script>
window.__forgotPasswordI18n = {
emailRequired: {{ _('auth.email_required')|tojson }},
invalidEmail: {{ _('auth.invalid_email')|tojson }},
forgotPasswordFailed: {{ _('auth.forgot_password_failed')|tojson }},
};
</script>
<!-- Forgot Password Logic -->
<script>
function languageSelector(currentLang, enabledLanguages) {
@@ -199,6 +208,7 @@
}
function forgotPassword() {
const i18n = window.__forgotPasswordI18n || {};
return {
// Data
email: '',
@@ -240,12 +250,12 @@
// Basic validation
if (!this.email) {
this.errors.email = 'Email is required';
this.errors.email = i18n.emailRequired;
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)) {
this.errors.email = 'Please enter a valid email address';
this.errors.email = i18n.invalidEmail;
return;
}
@@ -265,7 +275,7 @@
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Failed to send reset link');
throw new Error(data.detail || i18n.forgotPasswordFailed);
}
// Success - show email sent message
@@ -273,7 +283,7 @@
} catch (error) {
console.error('Forgot password error:', error);
this.showAlert(error.message || 'Failed to send reset link. Please try again.');
this.showAlert(error.message || i18n.forgotPasswordFailed);
} finally {
this.loading = false;
}

View File

@@ -157,7 +157,7 @@
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}">
&larr; {{ _("auth.continue_shopping") }}
&larr; {{ _("auth.back_to_home") }}
</a>
</p>

View File

@@ -1,7 +1,7 @@
{# app/templates/storefront/account/profile.html #}
{% extends "storefront/base.html" %}
{% block title %}My Profile - {{ store.name }}{% endblock %}
{% block title %}{{ _('customers.storefront.pages.profile.title') }} - {{ store.name }}{% endblock %}
{% block alpine_data %}shopProfilePage(){% endblock %}
@@ -11,19 +11,19 @@
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ base_url }}account/dashboard" class="hover:text-primary">My Account</a>
<a href="{{ base_url }}account/dashboard" class="hover:text-primary">{{ _('customers.storefront.pages.profile.breadcrumb_account') }}</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
<span class="text-gray-900 dark:text-white">Profile</span>
<span class="text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.breadcrumb_profile') }}</span>
</li>
</ol>
</nav>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Profile</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Manage your account information and preferences</p>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.title') }}</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.profile.subtitle') }}</p>
</div>
<!-- Loading State -->
@@ -58,15 +58,15 @@
<!-- Profile Information Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Profile Information</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">Update your personal details</p>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.info_section_title') }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.info_section_subtitle') }}</p>
</div>
<form @submit.prevent="saveProfile" class="p-6 space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- First Name -->
<div>
<label for="first_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name <span class="text-red-500">*</span>
{{ _('customers.first_name') }} <span class="text-red-500">*</span>
</label>
<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
@@ -78,7 +78,7 @@
<!-- Last Name -->
<div>
<label for="last_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name <span class="text-red-500">*</span>
{{ _('customers.last_name') }} <span class="text-red-500">*</span>
</label>
<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
@@ -91,7 +91,7 @@
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email Address <span class="text-red-500">*</span>
{{ _('customers.storefront.pages.profile.email_label') }} <span class="text-red-500">*</span>
</label>
<input type="email" id="email" x-model="profileForm.email" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
@@ -103,7 +103,7 @@
<!-- Phone -->
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Phone Number
{{ _('auth.phone_number') }}
</label>
<input type="tel" id="phone" x-model="profileForm.phone"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
@@ -122,7 +122,7 @@
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
style="background-color: var(--color-primary)">
<span x-show="savingProfile" class="-ml-1 mr-2 h-4 w-4" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
<span x-text="savingProfile ? 'Saving...' : 'Save Changes'"></span>
<span x-text="savingProfile ? i18n.saving : i18n.saveChanges"></span>
</button>
</div>
</form>
@@ -131,25 +131,25 @@
<!-- Preferences Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Preferences</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your account preferences</p>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.prefs_section_title') }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.prefs_section_subtitle') }}</p>
</div>
<form @submit.prevent="savePreferences" class="p-6 space-y-6">
<!-- Language -->
<div>
<label for="language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Preferred Language
{{ _('customers.storefront.pages.profile.preferred_language') }}
</label>
<select id="language" x-model="preferencesForm.preferred_language"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
focus:ring-2 focus:ring-primary focus:border-transparent
dark:bg-gray-700 dark:text-white"
style="--tw-ring-color: var(--color-primary)">
<option value="">Use shop default</option>
<option value="">{{ _('customers.storefront.pages.profile.use_shop_default') }}</option>
<option value="en">English</option>
<option value="fr">Francais</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="lb">Letzebuergesch</option>
<option value="lb">Lëtzebuergesch</option>
</select>
</div>
@@ -164,10 +164,10 @@
</div>
<div class="ml-3">
<label for="marketing_consent" class="text-sm font-medium text-gray-700 dark:text-gray-300">
Marketing Communications
{{ _('customers.storefront.pages.profile.marketing_communications') }}
</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
Receive emails about new products, offers, and promotions
{{ _('customers.storefront.pages.profile.marketing_desc') }}
</p>
</div>
</div>
@@ -182,7 +182,7 @@
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
style="background-color: var(--color-primary)">
<span x-show="savingPreferences" class="-ml-1 mr-2 h-4 w-4" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
<span x-text="savingPreferences ? 'Saving...' : 'Save Preferences'"></span>
<span x-text="savingPreferences ? i18n.saving : i18n.savePreferences"></span>
</button>
</div>
</form>
@@ -191,14 +191,14 @@
<!-- Change Password Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Change Password</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">Update your account password</p>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('auth.change_password') }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.change_password_subtitle') }}</p>
</div>
<form @submit.prevent="changePassword" class="p-6 space-y-6">
<!-- Current Password -->
<div>
<label for="current_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Current Password <span class="text-red-500">*</span>
{{ _('auth.current_password') }} <span class="text-red-500">*</span>
</label>
<input type="password" id="current_password" x-model="passwordForm.current_password" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
@@ -210,7 +210,7 @@
<!-- New Password -->
<div>
<label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password <span class="text-red-500">*</span>
{{ _('auth.new_password') }} <span class="text-red-500">*</span>
</label>
<input type="password" id="new_password" x-model="passwordForm.new_password" required
minlength="8"
@@ -219,14 +219,14 @@
dark:bg-gray-700 dark:text-white"
style="--tw-ring-color: var(--color-primary)">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Must be at least 8 characters with at least one letter and one number
{{ _('auth.password_requirements') }}
</p>
</div>
<!-- Confirm Password -->
<div>
<label for="confirm_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password <span class="text-red-500">*</span>
{{ _('auth.confirm_password') }} <span class="text-red-500">*</span>
</label>
<input type="password" id="confirm_password" x-model="passwordForm.confirm_password" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
@@ -235,7 +235,7 @@
style="--tw-ring-color: var(--color-primary)">
<p x-show="passwordForm.confirm_password && passwordForm.new_password !== passwordForm.confirm_password"
class="mt-1 text-xs text-red-500">
Passwords do not match
{{ _('auth.passwords_do_not_match') }}
</p>
</div>
@@ -252,7 +252,7 @@
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
style="background-color: var(--color-primary)">
<span x-show="changingPassword" class="-ml-1 mr-2 h-4 w-4" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
<span x-text="changingPassword ? 'Changing...' : 'Change Password'"></span>
<span x-text="changingPassword ? i18n.changing : i18n.changePassword"></span>
</button>
</div>
</form>
@@ -260,22 +260,22 @@
<!-- Account Info (read-only) -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Account Information</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">
<div>
<dt class="text-gray-500 dark:text-gray-400">Customer Number</dt>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.customer_number') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.customer_number || '-'"></dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Member Since</dt>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.member_since') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatDate(profile?.created_at)"></dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Total Orders</dt>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.total_orders') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.total_orders || 0"></dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Total Spent</dt>
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.total_spent') }}</dt>
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatPrice(profile?.total_spent)"></dd>
</div>
</dl>
@@ -286,9 +286,26 @@
{% block extra_scripts %}
<script>
window.__shopProfileI18n = {
saving: {{ _('customers.storefront.pages.profile.saving')|tojson }},
saveChanges: {{ _('customers.storefront.pages.profile.save_changes')|tojson }},
savePreferences: {{ _('customers.storefront.pages.profile.save_preferences')|tojson }},
changing: {{ _('customers.storefront.pages.profile.changing')|tojson }},
changePassword: {{ _('auth.change_password')|tojson }},
profileUpdated: {{ _('customers.storefront.pages.profile.profile_updated')|tojson }},
preferencesUpdated: {{ _('customers.storefront.pages.profile.preferences_updated')|tojson }},
passwordChanged: {{ _('customers.storefront.pages.profile.password_changed')|tojson }},
failedToLoad: {{ _('customers.storefront.pages.profile.failed_to_load')|tojson }},
failedToSaveProfile: {{ _('customers.storefront.pages.profile.failed_to_save_profile')|tojson }},
failedToSavePreferences: {{ _('customers.storefront.pages.profile.failed_to_save_preferences')|tojson }},
failedToChangePassword: {{ _('customers.storefront.pages.profile.failed_to_change_password')|tojson }},
passwordsDoNotMatch: {{ _('auth.passwords_do_not_match')|tojson }},
};
function shopProfilePage() {
const i18n = window.__shopProfileI18n || {};
return {
...storefrontLayoutData(),
i18n,
// State
profile: null,
@@ -347,7 +364,7 @@ function shopProfilePage() {
window.location.href = '{{ base_url }}account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
throw new Error('Failed to load profile');
throw new Error(i18n.failedToLoad);
}
this.profile = await response.json();
@@ -366,7 +383,7 @@ function shopProfilePage() {
} catch (err) {
console.error('Error loading profile:', err);
this.error = err.message || 'Failed to load profile';
this.error = err.message || i18n.failedToLoad;
} finally {
this.loading = false;
}
@@ -395,11 +412,11 @@ function shopProfilePage() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save profile');
throw new Error(error.detail || i18n.failedToSaveProfile);
}
this.profile = await response.json();
this.successMessage = 'Profile updated successfully';
this.successMessage = i18n.profileUpdated;
// Update localStorage user data
const userStr = localStorage.getItem('customer_user');
@@ -415,7 +432,7 @@ function shopProfilePage() {
} catch (err) {
console.error('Error saving profile:', err);
this.error = err.message || 'Failed to save profile';
this.error = err.message || i18n.failedToSaveProfile;
} finally {
this.savingProfile = false;
}
@@ -444,16 +461,16 @@ function shopProfilePage() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save preferences');
throw new Error(error.detail || i18n.failedToSavePreferences);
}
this.profile = await response.json();
this.successMessage = 'Preferences updated successfully';
this.successMessage = i18n.preferencesUpdated;
setTimeout(() => this.successMessage = '', 5000);
} catch (err) {
console.error('Error saving preferences:', err);
this.error = err.message || 'Failed to save preferences';
this.error = err.message || i18n.failedToSavePreferences;
} finally {
this.savingPreferences = false;
}
@@ -461,7 +478,7 @@ function shopProfilePage() {
async changePassword() {
if (this.passwordForm.new_password !== this.passwordForm.confirm_password) {
this.passwordError = 'Passwords do not match';
this.passwordError = i18n.passwordsDoNotMatch;
return;
}
@@ -487,7 +504,7 @@ function shopProfilePage() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to change password');
throw new Error(error.detail || i18n.failedToChangePassword);
}
// Clear password form
@@ -497,12 +514,12 @@ function shopProfilePage() {
confirm_password: ''
};
this.successMessage = 'Password changed successfully';
this.successMessage = i18n.passwordChanged;
setTimeout(() => this.successMessage = '', 5000);
} catch (err) {
console.error('Error changing password:', err);
this.passwordError = err.message || 'Failed to change password';
this.passwordError = err.message || i18n.failedToChangePassword;
} finally {
this.changingPassword = false;
}

View File

@@ -1,11 +1,11 @@
{# app/templates/storefront/account/reset-password.html #}
{# standalone #}
<!DOCTYPE html>
<html :class="{ 'dark': dark }" x-data="resetPassword()" lang="en">
<html :class="{ 'dark': dark }" x-data="resetPassword()" lang="{{ current_language|default('fr') }}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reset Password - {{ store.name }}</title>
<title>{{ _("auth.reset_password") }} - {{ store.name }}</title>
<!-- Fonts: Local fallback + Google Fonts -->
<link href="{{ static_v(request, 'static', path='shared/fonts/inter.css') }}" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
@@ -57,7 +57,7 @@
<div class="text-6xl mb-4">🔑</div>
{% endif %}
<h2 class="text-2xl font-bold text-white mb-2">{{ store.name }}</h2>
<p class="text-white opacity-90">Create new password</p>
<p class="text-white opacity-90">{{ _("auth.reset_password_subtitle") }}</p>
</div>
</div>
@@ -68,21 +68,22 @@
<template x-if="tokenInvalid">
<div class="text-center">
<div class="flex items-center justify-center w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900">
<span class="w-8 h-8 text-red-600 dark:text-red-400" x-html="$icon('x-mark', 'w-8 h-8')"></span>
<svg class="w-8 h-8 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Invalid or Expired Link
{{ _("auth.invalid_or_expired_link") }}
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
Please request a new password reset link.
{{ _("auth.invalid_or_expired_link_desc") }}
</p>
<a href="{{ base_url }}account/forgot-password"
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
Request New Link
{{ _("auth.request_new_link") }}
</a>
</div>
</template>
@@ -91,11 +92,11 @@
<template x-if="!tokenInvalid && !resetComplete">
<div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Reset Your Password
{{ _("auth.reset_your_password") }}
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
Enter your new password below. Password must be at least 8 characters.
{{ _("auth.reset_password_form_desc") }}
</p>
<!-- Error Message -->
@@ -107,14 +108,14 @@
<!-- Reset Password Form -->
<form @submit.prevent="handleSubmit">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">New Password</span>
<span class="text-gray-700 dark:text-gray-400">{{ _("auth.new_password") }}</span>
<input x-model="password"
:disabled="loading"
@input="clearErrors"
type="password"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.password }"
placeholder="Enter new password"
placeholder="{{ _('auth.new_password_placeholder') }}"
autocomplete="new-password"
required />
<span x-show="errors.password" x-text="errors.password"
@@ -122,14 +123,14 @@
</label>
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Confirm Password</span>
<span class="text-gray-700 dark:text-gray-400">{{ _("auth.confirm_password") }}</span>
<input x-model="confirmPassword"
:disabled="loading"
@input="clearErrors"
type="password"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.confirmPassword }"
placeholder="Confirm new password"
placeholder="{{ _('auth.confirm_password_placeholder') }}"
autocomplete="new-password"
required />
<span x-show="errors.confirmPassword" x-text="errors.confirmPassword"
@@ -138,10 +139,13 @@
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-6 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Reset Password</span>
<span x-show="!loading">{{ _("auth.reset_password_btn") }}</span>
<span x-show="loading" class="flex items-center justify-center">
<span class="inline w-4 h-4 mr-2" x-html="$icon('spinner', 'w-4 h-4 animate-spin')"></span>
Resetting...
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ _("auth.resetting") }}
</span>
</button>
</form>
@@ -152,21 +156,22 @@
<template x-if="resetComplete">
<div class="text-center">
<div class="flex items-center justify-center w-16 h-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900">
<span class="w-8 h-8 text-green-600 dark:text-green-400" x-html="$icon('check', 'w-8 h-8')"></span>
<svg class="w-8 h-8 text-green-600 dark:text-green-400" 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>
</div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Password Reset Complete
{{ _("auth.password_reset_complete") }}
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
Your password has been successfully reset.
You can now sign in with your new password.
{{ _("auth.password_reset_success_desc") }}
</p>
<a href="{{ base_url }}account/login"
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
Sign In
{{ _("auth.sign_in") }}
</a>
</div>
</template>
@@ -174,19 +179,34 @@
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _("auth.remember_password") }}</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}account/login">
Sign in
{{ _("auth.sign_in") }}
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}">
← Continue shopping
&larr; {{ _("auth.back_to_home") }}
</a>
</p>
<!-- Language selector -->
<div class="flex items-center justify-center gap-2 mt-6"
x-data='languageSelector("{{ request.state.language|default("fr") }}", {{ ["en", "fr", "de", "lb"]|tojson }})'>
<template x-for="lang in languages" :key="lang">
<button
@click="setLanguage(lang)"
class="px-2.5 py-1 text-sm font-semibold rounded-md transition-all"
:class="currentLang === lang
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 ring-1 ring-purple-300 dark:ring-purple-700'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
x-text="lang.toUpperCase()"
></button>
</template>
</div>
</div>
</div>
</div>
@@ -196,9 +216,37 @@
<!-- Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
{# Translated client-side strings — kept in sync with auth.* keys above #}
<script>
window.__resetPasswordI18n = {
passwordRequired: {{ _('auth.password_required')|tojson }},
passwordTooShort: {{ _('auth.password_too_short')|tojson }},
pleaseConfirmPassword: {{ _('auth.please_confirm_password')|tojson }},
passwordsDoNotMatch: {{ _('auth.passwords_do_not_match')|tojson }},
resetPasswordFailed: {{ _('auth.reset_password_failed')|tojson }},
};
</script>
<!-- Reset Password Logic -->
<script>
function languageSelector(currentLang, enabledLanguages) {
return {
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
async setLanguage(lang) {
if (lang === this.currentLang) return;
await fetch('/api/v1/platform/language/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: lang }),
});
window.location.reload();
},
};
}
function resetPassword() {
const i18n = window.__resetPasswordI18n || {};
return {
// Data
token: '',
@@ -251,22 +299,22 @@
// Validation
if (!this.password) {
this.errors.password = 'Password is required';
this.errors.password = i18n.passwordRequired;
return;
}
if (this.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
this.errors.password = i18n.passwordTooShort;
return;
}
if (!this.confirmPassword) {
this.errors.confirmPassword = 'Please confirm your password';
this.errors.confirmPassword = i18n.pleaseConfirmPassword;
return;
}
if (this.password !== this.confirmPassword) {
this.errors.confirmPassword = 'Passwords do not match';
this.errors.confirmPassword = i18n.passwordsDoNotMatch;
return;
}
@@ -294,7 +342,7 @@
return;
}
}
throw new Error(data.detail || 'Failed to reset password');
throw new Error(data.detail || i18n.resetPasswordFailed);
}
// Success
@@ -302,7 +350,7 @@
} catch (error) {
console.error('Reset password error:', error);
this.showAlert(error.message || 'Failed to reset password. Please try again.');
this.showAlert(error.message || i18n.resetPasswordFailed);
} finally {
this.loading = false;
}

View File

@@ -94,6 +94,7 @@ def self_enroll(
customer_name=data.customer_name,
customer_phone=data.customer_phone,
customer_birthday=data.customer_birthday,
customer_language=getattr(request.state, "language", None),
)
logger.info(f"Self-enrollment for customer {customer_id} at store {store.subdomain}")

View File

@@ -188,6 +188,7 @@ class CardService:
customer_name: str | None = None,
customer_phone: str | None = None,
customer_birthday: date | None = None,
customer_language: str | None = None,
) -> int:
"""
Resolve a customer ID from either a direct ID or email lookup.
@@ -224,11 +225,18 @@ class CardService:
customer = customer_service.get_customer_by_email(db, store_id, email)
if customer:
# Backfill birthday on existing customer if they didn't have
# one before — keeps the enrollment form useful for returning
# customers who never previously provided a birthday.
# Backfill birthday + preferred_language on existing customer
# if they were missing — keeps the enrollment form useful for
# returning customers and lets transactional emails (welcome,
# password reset) hit the right locale.
dirty = False
if customer_birthday and not customer.birth_date:
customer.birth_date = customer_birthday
dirty = True
if customer_language and not customer.preferred_language:
customer.preferred_language = customer_language
dirty = True
if dirty:
db.flush()
return customer.id
@@ -250,8 +258,17 @@ class CardService:
.first()
)
if existing_cardholder:
dirty = False
if customer_birthday and not existing_cardholder.birth_date:
existing_cardholder.birth_date = customer_birthday
dirty = True
if (
customer_language
and not existing_cardholder.preferred_language
):
existing_cardholder.preferred_language = customer_language
dirty = True
if dirty:
db.flush()
return existing_cardholder.id
@@ -272,6 +289,7 @@ class CardService:
last_name=last_name,
phone=customer_phone,
birth_date=customer_birthday,
preferred_language=customer_language,
)
logger.info(
f"Created customer {customer.id} ({email}) "

View File

@@ -123,7 +123,7 @@
"visit_platform": "Besuchen Sie unsere Plattform",
"already_have_account": "Haben Sie bereits ein Konto?",
"create_account": "Konto erstellen",
"continue_shopping": "Weiter einkaufen",
"back_to_home": "Zurück zur Startseite",
"admin_login": "Admin-Anmeldung",
"merchant_login": "Händler-Anmeldung",
"store_login": "Shop-Portal-Anmeldung",
@@ -140,7 +140,27 @@
"reset_link_sent": "Wir haben einen Link zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang und klicken Sie auf den Link.",
"didnt_receive_email": "E-Mail nicht erhalten? Überprüfen Sie Ihren Spam-Ordner oder",
"try_again": "versuchen Sie es erneut",
"remember_password": "Passwort wieder eingefallen?"
"remember_password": "Passwort wieder eingefallen?",
"reset_password_subtitle": "Neues Passwort erstellen",
"reset_your_password": "Passwort zurücksetzen",
"reset_password_form_desc": "Geben Sie unten Ihr neues Passwort ein. Es muss mindestens 8 Zeichen lang sein.",
"new_password_placeholder": "Neues Passwort eingeben",
"confirm_password_placeholder": "Neues Passwort bestätigen",
"resetting": "Wird zurückgesetzt...",
"reset_password_btn": "Passwort zurücksetzen",
"password_reset_complete": "Passwort zurückgesetzt",
"password_reset_success_desc": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit dem neuen Passwort anmelden.",
"invalid_or_expired_link": "Ungültiger oder abgelaufener Link",
"invalid_or_expired_link_desc": "Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.",
"request_new_link": "Neuen Link anfordern",
"email_required": "E-Mail ist erforderlich",
"invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"forgot_password_failed": "Link konnte nicht gesendet werden. Bitte erneut versuchen.",
"password_required": "Passwort ist erforderlich",
"password_too_short": "Das Passwort muss mindestens 8 Zeichen lang sein",
"please_confirm_password": "Bitte bestätigen Sie Ihr Passwort",
"passwords_do_not_match": "Passwörter stimmen nicht überein",
"reset_password_failed": "Passwort konnte nicht zurückgesetzt werden. Bitte erneut versuchen."
},
"nav": {
"dashboard": "Dashboard",

View File

@@ -123,7 +123,7 @@
"visit_platform": "Visit our platform",
"already_have_account": "Already have an account?",
"create_account": "Create an account",
"continue_shopping": "Continue shopping",
"back_to_home": "Back to Home",
"admin_login": "Admin Login",
"merchant_login": "Merchant Login",
"store_login": "Store Portal Login",
@@ -140,7 +140,27 @@
"reset_link_sent": "We've sent a password reset link to your email. Please check your inbox and click the link to reset your password.",
"didnt_receive_email": "Didn't receive the email? Check your spam folder or",
"try_again": "try again",
"remember_password": "Remember your password?"
"remember_password": "Remember your password?",
"reset_password_subtitle": "Create new password",
"reset_your_password": "Reset Your Password",
"reset_password_form_desc": "Enter your new password below. Password must be at least 8 characters.",
"new_password_placeholder": "Enter new password",
"confirm_password_placeholder": "Confirm new password",
"resetting": "Resetting...",
"reset_password_btn": "Reset Password",
"password_reset_complete": "Password Reset Complete",
"password_reset_success_desc": "Your password has been successfully reset. You can now sign in with your new password.",
"invalid_or_expired_link": "Invalid or Expired Link",
"invalid_or_expired_link_desc": "This password reset link is invalid or has expired. Please request a new password reset link.",
"request_new_link": "Request New Link",
"email_required": "Email is required",
"invalid_email": "Please enter a valid email address",
"forgot_password_failed": "Failed to send reset link. Please try again.",
"password_required": "Password is required",
"password_too_short": "Password must be at least 8 characters",
"please_confirm_password": "Please confirm your password",
"passwords_do_not_match": "Passwords do not match",
"reset_password_failed": "Failed to reset password. Please try again."
},
"nav": {
"dashboard": "Dashboard",

View File

@@ -123,7 +123,7 @@
"visit_platform": "Visitez notre plateforme",
"already_have_account": "Vous avez déjà un compte ?",
"create_account": "Créer un compte",
"continue_shopping": "Continuer vos achats",
"back_to_home": "Retour à l'accueil",
"admin_login": "Connexion Admin",
"merchant_login": "Connexion Marchand",
"store_login": "Connexion Portail Magasin",
@@ -140,7 +140,27 @@
"reset_link_sent": "Nous avons envoyé un lien de réinitialisation à votre adresse e-mail. Veuillez vérifier votre boîte de réception et cliquer sur le lien.",
"didnt_receive_email": "Vous n'avez pas reçu l'e-mail ? Vérifiez votre dossier spam ou",
"try_again": "réessayez",
"remember_password": "Vous vous souvenez de votre mot de passe ?"
"remember_password": "Vous vous souvenez de votre mot de passe ?",
"reset_password_subtitle": "Créer un nouveau mot de passe",
"reset_your_password": "Réinitialisez votre mot de passe",
"reset_password_form_desc": "Saisissez votre nouveau mot de passe ci-dessous. Il doit comporter au moins 8 caractères.",
"new_password_placeholder": "Saisissez le nouveau mot de passe",
"confirm_password_placeholder": "Confirmez le nouveau mot de passe",
"resetting": "Réinitialisation...",
"reset_password_btn": "Réinitialiser le mot de passe",
"password_reset_complete": "Mot de passe réinitialisé",
"password_reset_success_desc": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.",
"invalid_or_expired_link": "Lien invalide ou expiré",
"invalid_or_expired_link_desc": "Ce lien de réinitialisation est invalide ou a expiré. Veuillez en demander un nouveau.",
"request_new_link": "Demander un nouveau lien",
"email_required": "L'e-mail est obligatoire",
"invalid_email": "Veuillez saisir une adresse e-mail valide",
"forgot_password_failed": "Échec de l'envoi du lien. Veuillez réessayer.",
"password_required": "Le mot de passe est obligatoire",
"password_too_short": "Le mot de passe doit contenir au moins 8 caractères",
"please_confirm_password": "Veuillez confirmer votre mot de passe",
"passwords_do_not_match": "Les mots de passe ne correspondent pas",
"reset_password_failed": "Échec de la réinitialisation. Veuillez réessayer."
},
"nav": {
"dashboard": "Tableau de bord",

View File

@@ -123,7 +123,7 @@
"visit_platform": "Besicht eis Plattform",
"already_have_account": "Hutt Dir schonn e Kont?",
"create_account": "E Kont erstellen",
"continue_shopping": "Weider akafen",
"back_to_home": "Zréck op d'Haaptsäit",
"admin_login": "Admin Login",
"merchant_login": "Händler Login",
"store_login": "Buttek-Portal Login",
@@ -140,7 +140,27 @@
"reset_link_sent": "Mir hunn e Link fir d'Passwuert zréckzesetzen op Är E-Mail-Adress geschéckt. Kuckt w.e.g. Ären Posteingang a klickt op de Link.",
"didnt_receive_email": "E-Mail net kritt? Kuckt Ären Spam-Dossier oder",
"try_again": "probéiert et nach eng Kéier",
"remember_password": "Passwuert erëm agefall?"
"remember_password": "Passwuert erëm agefall?",
"reset_password_subtitle": "Neit Passwuert erstellen",
"reset_your_password": "Ärt Passwuert zrécksetzen",
"reset_password_form_desc": "Gitt hei drënner Ärt neit Passwuert an. Et muss mindestens 8 Zeechen laang sinn.",
"new_password_placeholder": "Neit Passwuert agi",
"confirm_password_placeholder": "Neit Passwuert bestätegen",
"resetting": "Gëtt zréckgesat...",
"reset_password_btn": "Passwuert zrécksetzen",
"password_reset_complete": "Passwuert zréckgesat",
"password_reset_success_desc": "Ärt Passwuert ass erfollegräich zréckgesat. Dir kënnt Iech elo mat Ärem neie Passwuert aloggen.",
"invalid_or_expired_link": "Ongëltege oder ofgelafenen Link",
"invalid_or_expired_link_desc": "Dëse Link fir d'Passwuert zréckzesetzen ass ongëlteg oder ofgelaf. Bitt e neie Link un.",
"request_new_link": "Neie Link ufroen",
"email_required": "E-Mail ass obligatoresch",
"invalid_email": "Gitt w.e.g. eng gëlteg E-Mail-Adress un",
"forgot_password_failed": "Link konnt net geschéckt ginn. Probéiert nach eng Kéier.",
"password_required": "Passwuert ass obligatoresch",
"password_too_short": "D'Passwuert muss mindestens 8 Zeechen hunn",
"please_confirm_password": "Bestätegt w.e.g. Ärt Passwuert",
"passwords_do_not_match": "D'Passwierder stëmmen net iwwerteneen",
"reset_password_failed": "Passwuert konnt net zréckgesat ginn. Probéiert nach eng Kéier."
},
"nav": {
"dashboard": "Dashboard",