fix(storefront): i18n sweep + locale-aware reset-password and welcome email
Some checks failed
Some checks failed
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:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }}">
|
||||
← {{ _("auth.continue_shopping") }}
|
||||
← {{ _("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;
|
||||
}
|
||||
|
||||
@@ -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 }}">
|
||||
← {{ _("auth.continue_shopping") }}
|
||||
← {{ _("auth.back_to_home") }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
← {{ _("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;
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}) "
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user