From 531114846792ba843e0f5ac6fab7029c184e41d0 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Thu, 12 Mar 2026 12:13:26 +0100 Subject: [PATCH 01/28] Add i18n support with next-intl for EN, PL, and DE locales Integrate next-intl for full internationalization across the storefront, checkout, account, and cart. Translate all hardcoded English strings including filters, sort options, price buckets, order statuses, address forms, and cart drawer. Use locale-aware currency formatting and date formatting. Fix locale switching via hard navigation in CountrySwitcher. DRY up store name/description with centralized getStoreName/getStoreDescription. Co-Authored-By: Claude Opus 4.6 --- messages/de.json | 382 ++++++++ messages/en.json | 382 ++++++++ messages/pl.json | 382 ++++++++ next.config.ts | 9 +- package-lock.json | 888 +++++++++++++++--- package.json | 4 +- scripts/check-locale-parity.ts | 79 ++ .../(checkout)/checkout/[id]/page.tsx | 39 +- .../[country]/[locale]/(checkout)/layout.tsx | 16 +- .../(checkout)/order-placed/[id]/page.tsx | 46 +- .../(storefront)/account/addresses/page.tsx | 27 +- .../account/credit-cards/page.tsx | 31 +- .../(storefront)/account/gift-cards/page.tsx | 80 +- .../[locale]/(storefront)/account/layout.tsx | 35 +- .../(storefront)/account/orders/[id]/page.tsx | 54 +- .../(storefront)/account/orders/page.tsx | 73 +- .../[locale]/(storefront)/account/page.tsx | 44 +- .../(storefront)/account/profile/page.tsx | 25 +- .../(storefront)/account/register/page.tsx | 35 +- .../CategoryProductsContent.tsx | 4 +- .../(storefront)/c/[...permalink]/page.tsx | 4 +- .../[locale]/(storefront)/cart/page.tsx | 43 +- .../[country]/[locale]/(storefront)/page.tsx | 17 +- .../(storefront)/products/ProductsContent.tsx | 17 +- .../products/[slug]/ProductDetails.tsx | 24 +- src/app/layout.tsx | 30 +- src/components/cart/CartDrawer.tsx | 32 +- src/components/checkout/AddressEditModal.tsx | 11 +- src/components/checkout/AddressFormFields.tsx | 45 +- src/components/checkout/AddressSelector.tsx | 11 +- src/components/checkout/AddressStep.tsx | 26 +- src/components/checkout/CouponCode.tsx | 17 +- src/components/checkout/DeliveryStep.tsx | 29 +- src/components/checkout/OrderSummary.tsx | 16 +- src/components/checkout/PaymentStep.tsx | 42 +- src/components/layout/CountrySwitcher.tsx | 13 +- src/components/layout/Footer.tsx | 29 +- src/components/layout/Header.tsx | 6 +- src/components/navigation/Breadcrumbs.tsx | 6 +- src/components/products/MediaGallery.tsx | 10 +- src/components/products/ProductCard.tsx | 6 +- src/components/products/ProductCarousel.tsx | 6 +- src/components/products/ProductGrid.tsx | 6 +- .../products/ProductListingLayout.tsx | 14 +- src/components/products/VariantPicker.tsx | 4 +- .../products/__tests__/ProductCard.test.tsx | 10 +- .../filters/AvailabilityDropdownContent.tsx | 15 +- .../products/filters/FilterChips.tsx | 12 +- .../products/filters/MobileFilterDrawer.tsx | 27 +- .../products/filters/ProductFilters.tsx | 18 +- .../products/filters/SortDropdownContent.tsx | 7 +- src/components/search/SearchBar.tsx | 10 +- src/contexts/StoreContext.tsx | 8 +- src/i18n/request.ts | 30 + src/lib/utils/filters.ts | 53 +- src/lib/utils/price-buckets.ts | 30 +- src/types/next-intl.d.ts | 7 + 57 files changed, 2753 insertions(+), 573 deletions(-) create mode 100644 messages/de.json create mode 100644 messages/en.json create mode 100644 messages/pl.json create mode 100644 scripts/check-locale-parity.ts create mode 100644 src/i18n/request.ts create mode 100644 src/types/next-intl.d.ts diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 00000000..4e7e2886 --- /dev/null +++ b/messages/de.json @@ -0,0 +1,382 @@ +{ + "common": { + "subtotal": "Zwischensumme", + "shipping": "Versand", + "tax": "Steuer", + "discount": "Rabatt", + "total": "Gesamt", + "adjustments": "Anpassungen", + "edit": "Bearbeiten", + "remove": "Entfernen", + "cancel": "Abbrechen", + "back": "Zurück", + "continue": "Weiter", + "loading": "Laden...", + "apply": "Anwenden", + "saving": "Speichern...", + "processing": "Verarbeitung...", + "continueShopping": "Weiter einkaufen", + "orderSummary": "Bestellübersicht", + "selectCountry": "Land auswählen", + "myAccount": "Mein Konto" + }, + "header": { + "categories": "Kategorien", + "openCart": "Warenkorb öffnen", + "account": "Konto" + }, + "footer": { + "description": "Ein moderner E-Commerce-Shop powered by Spree Commerce und Next.js.", + "shop": "Shop", + "allProducts": "Alle Produkte", + "categories": "Kategorien", + "account": "Konto", + "myAccount": "Mein Konto", + "orderHistory": "Bestellverlauf", + "cart": "Warenkorb", + "copyright": "© {year} {storeName}. Unterstützt durch Spree Commerce." + }, + "home": { + "welcome": "Willkommen bei Spree Store", + "heroDescription": "Entdecken Sie großartige Produkte mit unserem modernen E-Commerce-Erlebnis powered by Spree Commerce.", + "shopNow": "Jetzt einkaufen", + "browseCategories": "Kategorien durchsuchen", + "featuredProducts": "Ausgewählte Produkte", + "viewAll": "Alle ansehen", + "qualityProducts": "Qualitätsprodukte", + "qualityDescription": "Sorgfältig kuratierte Auswahl der besten Produkte.", + "fastShipping": "Schneller Versand", + "shippingDescription": "Schnelle und zuverlässige Lieferung bis an Ihre Haustür.", + "support": "24/7 Support", + "supportDescription": "Unser Team ist jederzeit für Sie da." + }, + "cart": { + "cart": "Warenkorb", + "closeCart": "Warenkorb schließen", + "emptyCart": "Ihr Warenkorb ist leer", + "emptyCartDescription": "Es sieht so aus, als hätten Sie noch nichts in Ihren Warenkorb gelegt.", + "removeItem": "Artikel entfernen", + "decreaseQuantity": "Menge verringern", + "increaseQuantity": "Menge erhöhen", + "shippingNote": "Versand und Steuern werden an der Kasse berechnet", + "checkout": "Zur Kasse", + "viewCart": "Warenkorb anzeigen", + "shoppingCart": "Warenkorb", + "proceedToCheckout": "Zur Kasse gehen", + "itemCount": "{count, plural, one {# Artikel} other {# Artikel}}", + "shippingCalculatedAtCheckout": "Wird an der Kasse berechnet" + }, + "products": { + "allProducts": "Alle Produkte", + "browseCollection": "Entdecken Sie unsere gesamte Kollektion", + "searchResultsFor": "Suchergebnisse für \"{query}\"", + "productsFound": "{count, plural, one {# Produkt} other {# Produkte}} gefunden", + "noProductsFound": "Keine Produkte gefunden", + "tryAdjustingFilters": "Versuchen Sie, Ihre Filter anzupassen", + "noMatchingProducts": "Wir konnten keine Produkte finden, die zu \"{query}\" passen", + "showingProducts": "{shown} von {total} Produkten werden angezeigt", + "loadingMore": "Weitere werden geladen...", + "noMoreProducts": "Keine weiteren Produkte zum Laden", + "filters": "Filter", + "sortBy": "Sortieren nach", + "resetFilters": "Filter zurücksetzen", + "price": "Preis", + "availability": "Verfügbarkeit", + "inStock": "Auf Lager", + "outOfStock": "Nicht auf Lager", + "min": "Min", + "max": "Max", + "range": "Bereich: {min} - {max}", + "clear": "Löschen", + "productCount": "{count, plural, one {# Produkt} other {# Produkte}}", + "sale": "Sale", + "outOfStockVariant": "(Nicht auf Lager)", + "addToCart": "In den Warenkorb", + "adding": "Wird hinzugefügt...", + "description": "Beschreibung", + "details": "Details", + "sku": "Artikelnr.", + "options": "Optionen", + "search": "Suche...", + "searching": "Suche läuft...", + "viewAllResultsFor": "Alle Ergebnisse für \"{query}\" anzeigen", + "manual": "Manuell", + "bestSelling": "Bestseller", + "priceLowHigh": "Preis (niedrig-hoch)", + "priceHighLow": "Preis (hoch-niedrig)", + "newest": "Neueste", + "oldest": "Älteste", + "nameAZ": "Name (A-Z)", + "nameZA": "Name (Z-A)", + "sortUnknown": "Sonstige", + "categories": "Kategorien", + "noCategoriesFound": "Keine Kategorien gefunden.", + "noCategoriesInGroup": "Keine Kategorien in dieser Gruppe.", + "subcategories": "{count, plural, one {# Unterkategorie} other {# Unterkategorien}}", + "noImageAvailable": "Kein Bild verfügbar", + "clickToZoom": "Zum Vergrößern klicken", + "noProductsMatchingFilters": "Keine Produkte gefunden, die Ihren Filtern entsprechen.", + "decreaseQuantity": "Menge verringern", + "increaseQuantity": "Menge erhöhen", + "carouselPrev": "Vorherige Produkte", + "carouselNext": "Nächste Produkte", + "lightboxClose": "Lightbox schließen", + "lightboxPrev": "Vorheriges Bild", + "lightboxNext": "Nächstes Bild", + "subcategoriesHeading": "Unterkategorien", + "sort": "Sortieren", + "clearAll": "Alle löschen", + "clearAllFiltersCount": "Alle Filter löschen ({count})", + "showResults": "Ergebnisse anzeigen", + "closeFilters": "Filter schließen", + "anyAvailability": "Beliebige Verfügbarkeit", + "customPrice": "Benutzerdefiniert", + "priceUnder": "Unter {price}", + "priceAbove": "{price}+", + "priceRangeBucket": "{min} – {max}" + }, + "navigation": { + "home": "Startseite", + "categories": "Kategorien", + "breadcrumb": "Brotkrümelnavigation" + }, + "checkout": { + "checkout": "Kasse", + "orderNumber": "Bestellung #{number}", + "checkoutError": "Kassenfehler", + "returnToCart": "Zurück zum Warenkorb", + "emptyCart": "Ihr Warenkorb ist leer", + "emptyCartDescription": "Fügen Sie einige Artikel zu Ihrem Warenkorb hinzu, bevor Sie zur Kasse gehen.", + "stepShipping": "Versand", + "stepDelivery": "Lieferung", + "stepPayment": "Zahlung", + "contactInformation": "Kontaktinformationen", + "emailAddress": "E-Mail-Adresse", + "emailPlaceholder": "sie@beispiel.de", + "usingAccountEmail": "Verwendung Ihrer Konto-E-Mail-Adresse", + "signInPrompt": "Haben Sie bereits ein Konto?", + "signIn": "Anmelden", + "signInDescription": "um auf Ihre gespeicherten Adressen und Bestellhistorie zuzugreifen.", + "shippingAddress": "Lieferadresse", + "continueToDelivery": "Weiter zur Lieferung", + "shippingMethod": "Versandmethode", + "loadingShippingOptions": "Versandoptionen werden geladen...", + "shipmentOf": "Sendung {current} von {total}", + "shipsFrom": "Versand von {location}", + "phone": "Telefon: {phone}", + "billingAddress": "Rechnungsadresse", + "sameAsShipping": "Wie Lieferadresse", + "paymentMethod": "Zahlungsmethode", + "loadingPaymentForm": "Zahlungsformular wird geladen...", + "addNewPaymentMethod": "Neue Zahlungsmethode hinzufügen", + "endingIn": "endet auf", + "exp": "Gültig bis", + "default": "Standard", + "noPaymentMethods": "Für diese Bestellung sind keine Zahlungsmethoden verfügbar.", + "payNow": "Jetzt bezahlen", + "failedToInitPayment": "Zahlung konnte nicht initialisiert werden. Bitte versuchen Sie es erneut.", + "failedToCreateSession": "Zahlungssitzung konnte nicht erstellt werden.", + "paymentError": "Bei der Zahlung ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", + "orderNotFound": "Bestellung nicht gefunden oder Sie haben keinen Zugriff darauf.", + "failedToLoadCheckout": "Kasse konnte nicht geladen werden. Bitte versuchen Sie es erneut.", + "orderNotFoundAfterPayment": "Bestellung nach Zahlung nicht gefunden. Bitte kontaktieren Sie den Support.", + "failedToSaveAddress": "Adresse konnte nicht gespeichert werden", + "failedToProceed": "Weiter zum nächsten Schritt fehlgeschlagen", + "generalError": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.", + "failedToSelectRate": "Versandrate konnte nicht ausgewählt werden", + "failedToSaveBilling": "Rechnungsadresse konnte nicht gespeichert werden", + "failedToSaveBillingRetry": "Rechnungsadresse konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.", + "failedToCompleteOrder": "Bestellung konnte nicht abgeschlossen werden", + "stripeNotLoaded": "Stripe wurde noch nicht geladen", + "paymentProcessingError": "Bei der Zahlung ist ein Fehler aufgetreten.", + "failedToCompletePaymentSession": "Zahlungssitzung konnte nicht abgeschlossen werden" + }, + "address": { + "firstName": "Vorname", + "lastName": "Nachname", + "company": "Firma (optional)", + "address": "Adresse", + "streetAddress": "Straßenadresse", + "apartment": "Wohnung, Suite usw. (optional)", + "city": "Stadt", + "country": "Land", + "selectCountry": "Land auswählen", + "stateProvince": "Bundesland / Provinz", + "selectState": "Bundesland auswählen", + "stateOrProvince": "Bundesland oder Provinz", + "zipCode": "Postleitzahl", + "phone": "Telefon (optional)", + "selectAnAddress": "Adresse auswählen", + "useDifferentAddress": "Andere Adresse verwenden", + "editAddress": "Adresse bearbeiten", + "addNewAddress": "Neue Adresse hinzufügen", + "saveAddress": "Adresse speichern", + "failedToSave": "Adresse konnte nicht gespeichert werden" + }, + "account": { + "myAccount": "Mein Konto", + "signInDescription": "Melden Sie sich an, um auf Ihr Konto und Ihre Bestellhistorie zuzugreifen.", + "email": "E-Mail", + "password": "Passwort", + "signIn": "Anmelden", + "signingIn": "Anmeldung...", + "invalidCredentials": "Ungültige E-Mail oder Passwort", + "dontHaveAccount": "Noch kein Konto?", + "signUp": "Registrieren", + "accountOverview": "Kontoübersicht", + "orderHistory": "Bestellverlauf", + "orderHistoryDescription": "Frühere Bestellungen ansehen und Sendungen verfolgen", + "addresses": "Adressen", + "addressesDescription": "Versand- und Rechnungsadressen verwalten", + "paymentMethods": "Zahlungsmethoden", + "paymentMethodsDescription": "Gespeicherte Kreditkarten verwalten", + "profile": "Profil", + "profileDescription": "Persönliche Daten aktualisieren", + "signOut": "Abmelden", + "overview": "Übersicht", + "orders": "Bestellungen", + "giftCards": "Geschenkkarten", + "addNewAddress": "Neue Adresse hinzufügen", + "noAddresses": "Keine gespeicherten Adressen", + "noAddressesDescription": "Adressen, die Sie beim Checkout hinzufügen, erscheinen hier.", + "defaultAddress": "Standard", + "deleteConfirm": "Sind Sie sicher, dass Sie diese Adresse löschen möchten?", + "setAsDefault": "Als Standard festlegen", + "showPassword": "Passwort anzeigen", + "hidePassword": "Passwort verbergen" + }, + "register": { + "createAccount": "Konto erstellen", + "signUpDescription": "Registrieren Sie sich, um mit dem Einkaufen zu beginnen.", + "confirmPassword": "Passwort bestätigen", + "creatingAccount": "Konto wird erstellt...", + "passwordsDontMatch": "Passwörter stimmen nicht überein", + "passwordTooShort": "Das Passwort muss mindestens 6 Zeichen lang sein", + "registrationFailed": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "alreadyHaveAccount": "Haben Sie bereits ein Konto?", + "signIn": "Anmelden" + }, + "orders": { + "orderHistory": "Bestellverlauf", + "noOrders": "Noch keine Bestellungen", + "noOrdersDescription": "Wenn Sie Bestellungen aufgeben, erscheinen sie hier.", + "startShopping": "Einkaufen starten", + "order": "Bestellung", + "date": "Datum", + "status": "Status", + "actions": "Aktionen", + "view": "Ansehen", + "orderNotFound": "Bestellung nicht gefunden", + "backToOrders": "Zurück zu Bestellungen", + "placedOn": "Bestellt am {date}", + "orderItems": "Bestellartikel", + "qty": "Menge: {quantity}", + "orderAgain": "Erneut bestellen", + "deliveryAddress": "Lieferadresse", + "shippingMethod": "Versandmethode", + "shippedFrom": "Versand von {location}", + "trackItems": "Sendung verfolgen", + "shipmentCanceled": "Diese Sendung wurde storniert.", + "noTrackingInfo": "Keine Tracking-Informationen vorhanden.", + "paymentInformation": "Zahlungsinformationen", + "cardEndingIn": "{label} endet auf {digits}", + "cardExpires": "Gültig bis {month}/{year}", + "storeCreditApplied": "Angewendet {amount} — {remaining} verbleibend", + "billingAddress": "Rechnungsadresse", + "orderNotFoundDescription": "Die Bestellung, die Sie suchen, existiert nicht.", + "storeCredit": "Store-Guthaben", + "specialInstructions": "Besondere Hinweise", + "payment": "Zahlung", + "shipment": "Sendung", + "shipped": "Versendet", + "partiallyShipped": "Teilweise versendet", + "delivered": "Zugestellt", + "ready": "Bereit", + "pending": "Ausstehend", + "canceled": "Storniert", + "backorder": "Nachbestellung", + "paid": "Bezahlt", + "balanceDue": "Offener Betrag", + "creditOwed": "Guthaben geschuldet", + "failed": "Fehlgeschlagen", + "void": "Storniert", + "notAvailable": "k. A." + }, + "orderPlaced": { + "thanksForOrder": "Vielen Dank für Ihre Bestellung, {name}!", + "thanksForOrderAnonymous": "Vielen Dank für Ihre Bestellung!", + "orderNumber": "Bestellung #{number}", + "emailConfirmation": "Sie erhalten in Kürze eine E-Mail-Bestätigung.", + "orderItems": "Bestellartikel", + "contactInformation": "Kontaktinformationen", + "shippingAddress": "Lieferadresse", + "billingAddress": "Rechnungsadresse", + "orderNotFound": "Bestellung nicht gefunden", + "failedToLoad": "Bestelldetails konnten nicht geladen werden." + }, + "coupon": { + "placeholder": "Geschenkkarte oder Rabattcode", + "apply": "Anwenden", + "remove": "Entfernen", + "invalidCode": "Ungültiger Gutscheincode", + "applyFailed": "Gutschein konnte nicht angewendet werden. Bitte versuchen Sie es erneut.", + "failedToRemove": "Gutscheincode konnte nicht entfernt werden", + "removeCoupon": "Gutschein {code} entfernen", + "applying": "Wird angewendet...", + "removing": "Wird entfernt..." + }, + "checkoutLayout": { + "backToStore": "Zurück zum Shop", + "showOrderSummary": "Bestellübersicht anzeigen", + "hideOrderSummary": "Bestellübersicht ausblenden", + "allRightsReserved": "© {year} {storeName}. Alle Rechte vorbehalten." + }, + "profile": { + "profile": "Profil", + "firstName": "Vorname", + "lastName": "Nachname", + "emailAddress": "E-Mail-Adresse", + "saveChanges": "Änderungen speichern", + "saving": "Speichern...", + "profileUpdated": "Profil erfolgreich aktualisiert!", + "accountInformation": "Kontoinformationen", + "accountId": "Konto-ID", + "loadingProfile": "Profil wird geladen...", + "failedToUpdate": "Profil konnte nicht aktualisiert werden" + }, + "creditCards": { + "paymentMethods": "Zahlungsmethoden", + "noCards": "Keine Zahlungsmethoden gespeichert", + "noCardsDescription": "Zahlungsmethoden werden automatisch gespeichert, wenn Sie einen Kauf tätigen.", + "expires": "Gültig bis", + "default": "Standard", + "removing": "Wird entfernt...", + "secureInfo": "Ihre Zahlungsinformationen werden sicher gespeichert.", + "deleteConfirm": "Sind Sie sicher, dass Sie diese Karte entfernen möchten?", + "cardMaskedLabel": "{label} endet auf {digits}, gültig bis {month}/{year}" + }, + "giftCards": { + "giftCards": "Geschenkkarten", + "noGiftCards": "Keine Geschenkkarten", + "noGiftCardsDescription": "Sie haben noch keine Geschenkkarten.", + "active": "Aktiv", + "partiallyUsed": "Teilweise eingelöst", + "fullyRedeemed": "Vollständig eingelöst", + "canceled": "Storniert", + "expired": "Abgelaufen", + "noExpiration": "Kein Ablaufdatum", + "remaining": "verbleibend", + "usedAmount": "Verwendet: {amount}", + "totalAmountWithValue": "Gesamt: {amount}", + "addedOnDate": "Hinzugefügt am {date}", + "fullyRedeemedOnDate": "Vollständig eingelöst am {date}", + "activeGiftCards": "Aktive Geschenkkarten", + "expiredRedeemed": "Abgelaufen / Eingelöst", + "copy": "Kopieren", + "copied": "Kopiert!", + "copyCodeToClipboard": "Code in die Zwischenablage kopieren", + "expiresOn": "Läuft ab am {date}", + "percentUsed": "{percent}% verwendet", + "giftCardsHelpText": "Geschenkkarten können beim Checkout zum Bezahlen Ihrer Bestellungen verwendet werden. Das verbleibende Guthaben wird für zukünftige Einkäufe gespeichert." + } +} diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 00000000..ec05253f --- /dev/null +++ b/messages/en.json @@ -0,0 +1,382 @@ +{ + "common": { + "subtotal": "Subtotal", + "shipping": "Shipping", + "tax": "Tax", + "discount": "Discount", + "total": "Total", + "adjustments": "Adjustments", + "edit": "Edit", + "remove": "Remove", + "cancel": "Cancel", + "back": "Back", + "continue": "Continue", + "loading": "Loading...", + "apply": "Apply", + "saving": "Saving...", + "processing": "Processing...", + "continueShopping": "Continue Shopping", + "orderSummary": "Order Summary", + "selectCountry": "Select Country", + "myAccount": "My Account" + }, + "header": { + "categories": "Categories", + "openCart": "Open cart", + "account": "Account" + }, + "footer": { + "description": "A modern e-commerce storefront powered by Spree Commerce and Next.js.", + "shop": "Shop", + "allProducts": "All Products", + "categories": "Categories", + "account": "Account", + "myAccount": "My Account", + "orderHistory": "Order History", + "cart": "Cart", + "copyright": "© {year} {storeName}. Powered by Spree Commerce." + }, + "home": { + "welcome": "Welcome to Spree Store", + "heroDescription": "Discover amazing products with our modern e-commerce experience powered by Spree Commerce.", + "shopNow": "Shop Now", + "browseCategories": "Browse Categories", + "featuredProducts": "Featured Products", + "viewAll": "View all", + "qualityProducts": "Quality Products", + "qualityDescription": "Carefully curated selection of the best products.", + "fastShipping": "Fast Shipping", + "shippingDescription": "Quick and reliable delivery to your doorstep.", + "support": "24/7 Support", + "supportDescription": "Our team is here to help you anytime." + }, + "cart": { + "cart": "Cart", + "closeCart": "Close cart", + "emptyCart": "Your cart is empty", + "emptyCartDescription": "Looks like you haven't added anything to your cart yet.", + "removeItem": "Remove item", + "decreaseQuantity": "Decrease quantity", + "increaseQuantity": "Increase quantity", + "shippingNote": "Shipping and taxes calculated at checkout", + "checkout": "Checkout", + "viewCart": "View Cart", + "shoppingCart": "Shopping Cart", + "proceedToCheckout": "Proceed to Checkout", + "itemCount": "{count, plural, one {# item} other {# items}}", + "shippingCalculatedAtCheckout": "Calculated at checkout" + }, + "products": { + "allProducts": "All Products", + "browseCollection": "Browse our complete collection", + "searchResultsFor": "Search results for \"{query}\"", + "productsFound": "{count, plural, one {# product} other {# products}} found", + "noProductsFound": "No products found", + "tryAdjustingFilters": "Try adjusting your filters", + "noMatchingProducts": "We couldn't find any products matching \"{query}\"", + "showingProducts": "Showing {shown} of {total} products", + "loadingMore": "Loading more...", + "noMoreProducts": "No more products to load", + "filters": "Filters", + "sortBy": "Sort by", + "resetFilters": "Reset filters", + "price": "Price", + "availability": "Availability", + "inStock": "In Stock", + "outOfStock": "Out of Stock", + "min": "Min", + "max": "Max", + "range": "Range: {min} - {max}", + "clear": "Clear", + "productCount": "{count, plural, one {# product} other {# products}}", + "sale": "Sale", + "outOfStockVariant": "(Out of stock)", + "addToCart": "Add to Cart", + "adding": "Adding...", + "description": "Description", + "details": "Details", + "sku": "SKU", + "options": "Options", + "search": "Search...", + "searching": "Searching...", + "viewAllResultsFor": "View all results for \"{query}\"", + "manual": "Manual", + "bestSelling": "Best Selling", + "priceLowHigh": "Price (low-high)", + "priceHighLow": "Price (high-low)", + "newest": "Newest", + "oldest": "Oldest", + "nameAZ": "Name (A-Z)", + "nameZA": "Name (Z-A)", + "sortUnknown": "Other", + "categories": "Categories", + "noCategoriesFound": "No categories found.", + "noCategoriesInGroup": "No categories in this group.", + "subcategories": "{count, plural, one {# subcategory} other {# subcategories}}", + "noImageAvailable": "No image available", + "clickToZoom": "Click to zoom", + "noProductsMatchingFilters": "No products found matching your filters.", + "decreaseQuantity": "Decrease quantity", + "increaseQuantity": "Increase quantity", + "carouselPrev": "Previous products", + "carouselNext": "Next products", + "lightboxClose": "Close lightbox", + "lightboxPrev": "Previous image", + "lightboxNext": "Next image", + "subcategoriesHeading": "Subcategories", + "sort": "Sort", + "clearAll": "Clear all", + "clearAllFiltersCount": "Clear all filters ({count})", + "showResults": "Show results", + "closeFilters": "Close filters", + "anyAvailability": "Any availability", + "customPrice": "Custom", + "priceUnder": "Under {price}", + "priceAbove": "{price}+", + "priceRangeBucket": "{min} – {max}" + }, + "navigation": { + "home": "Home", + "categories": "Categories", + "breadcrumb": "Breadcrumb" + }, + "checkout": { + "checkout": "Checkout", + "orderNumber": "Order #{number}", + "checkoutError": "Checkout Error", + "returnToCart": "Return to Cart", + "emptyCart": "Your Cart is Empty", + "emptyCartDescription": "Add some items to your cart before checking out.", + "stepShipping": "Shipping", + "stepDelivery": "Delivery", + "stepPayment": "Payment", + "contactInformation": "Contact Information", + "emailAddress": "Email address", + "emailPlaceholder": "you@example.com", + "usingAccountEmail": "Using your account email address", + "signInPrompt": "Already have an account?", + "signIn": "Sign in", + "signInDescription": "to access your saved addresses and order history.", + "shippingAddress": "Shipping Address", + "continueToDelivery": "Continue to Delivery", + "shippingMethod": "Shipping Method", + "loadingShippingOptions": "Loading shipping options...", + "shipmentOf": "Shipment {current} of {total}", + "shipsFrom": "Ships from {location}", + "phone": "Phone: {phone}", + "billingAddress": "Billing Address", + "sameAsShipping": "Same as shipping address", + "paymentMethod": "Payment Method", + "loadingPaymentForm": "Loading payment form...", + "addNewPaymentMethod": "Add new payment method", + "endingIn": "ending in", + "exp": "Exp", + "default": "Default", + "noPaymentMethods": "No payment methods available for this order.", + "payNow": "Pay Now", + "failedToInitPayment": "Failed to initialize payment. Please try again.", + "failedToCreateSession": "Failed to create payment session.", + "paymentError": "An error occurred during payment. Please try again.", + "orderNotFound": "Order not found or you don't have access to it.", + "failedToLoadCheckout": "Failed to load checkout. Please try again.", + "orderNotFoundAfterPayment": "Order not found after payment. Please contact support.", + "failedToSaveAddress": "Failed to save address", + "failedToProceed": "Failed to proceed to next step", + "generalError": "An error occurred. Please try again.", + "failedToSelectRate": "Failed to select shipping rate", + "failedToSaveBilling": "Failed to save billing address", + "failedToSaveBillingRetry": "Failed to save billing address. Please try again.", + "failedToCompleteOrder": "Failed to complete order", + "stripeNotLoaded": "Stripe has not loaded yet", + "paymentProcessingError": "An error occurred during payment.", + "failedToCompletePaymentSession": "Failed to complete payment session" + }, + "address": { + "firstName": "First name", + "lastName": "Last name", + "company": "Company (optional)", + "address": "Address", + "streetAddress": "Street address", + "apartment": "Apartment, suite, etc. (optional)", + "city": "City", + "country": "Country", + "selectCountry": "Select a country", + "stateProvince": "State / Province", + "selectState": "Select a state", + "stateOrProvince": "State or province", + "zipCode": "ZIP / Postal code", + "phone": "Phone (optional)", + "selectAnAddress": "Select an address", + "useDifferentAddress": "Use a different address", + "editAddress": "Edit Address", + "addNewAddress": "Add New Address", + "saveAddress": "Save Address", + "failedToSave": "Failed to save address" + }, + "account": { + "myAccount": "My Account", + "signInDescription": "Sign in to access your account and order history.", + "email": "Email", + "password": "Password", + "signIn": "Sign In", + "signingIn": "Signing in...", + "invalidCredentials": "Invalid email or password", + "dontHaveAccount": "Don't have an account?", + "signUp": "Sign up", + "accountOverview": "Account Overview", + "orderHistory": "Order History", + "orderHistoryDescription": "View your past orders and track shipments", + "addresses": "Addresses", + "addressesDescription": "Manage your shipping and billing addresses", + "paymentMethods": "Payment Methods", + "paymentMethodsDescription": "Manage your saved credit cards", + "profile": "Profile", + "profileDescription": "Update your personal information", + "signOut": "Sign Out", + "overview": "Overview", + "orders": "Orders", + "giftCards": "Gift Cards", + "addNewAddress": "Add New Address", + "noAddresses": "No saved addresses", + "noAddressesDescription": "Addresses you add during checkout will appear here.", + "defaultAddress": "Default", + "deleteConfirm": "Are you sure you want to delete this address?", + "setAsDefault": "Set as default", + "showPassword": "Show password", + "hidePassword": "Hide password" + }, + "register": { + "createAccount": "Create Account", + "signUpDescription": "Sign up to start shopping with us.", + "confirmPassword": "Confirm Password", + "creatingAccount": "Creating account...", + "passwordsDontMatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "registrationFailed": "Registration failed. Please try again.", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in" + }, + "orders": { + "orderHistory": "Order History", + "noOrders": "No orders yet", + "noOrdersDescription": "When you place orders, they will appear here.", + "startShopping": "Start Shopping", + "order": "Order", + "date": "Date", + "status": "Status", + "actions": "Actions", + "view": "View", + "orderNotFound": "Order not found", + "backToOrders": "Back to orders", + "placedOn": "Placed on {date}", + "orderItems": "Order Items", + "qty": "Qty: {quantity}", + "orderAgain": "Order again", + "deliveryAddress": "Delivery Address", + "shippingMethod": "Shipping Method", + "shippedFrom": "Shipped from {location}", + "trackItems": "Track Items", + "shipmentCanceled": "This shipment has been canceled.", + "noTrackingInfo": "No tracking information present.", + "paymentInformation": "Payment Information", + "cardEndingIn": "{label} ending in {digits}", + "cardExpires": "Expires {month}/{year}", + "storeCreditApplied": "Applied {amount} — {remaining} remaining", + "billingAddress": "Billing Address", + "orderNotFoundDescription": "The order you're looking for doesn't exist.", + "storeCredit": "Store Credit", + "specialInstructions": "Special Instructions", + "payment": "Payment", + "shipment": "Shipment", + "shipped": "Shipped", + "partiallyShipped": "Partially Shipped", + "delivered": "Delivered", + "ready": "Ready", + "pending": "Pending", + "canceled": "Canceled", + "backorder": "Backorder", + "paid": "Paid", + "balanceDue": "Balance Due", + "creditOwed": "Credit Owed", + "failed": "Failed", + "void": "Void", + "notAvailable": "N/A" + }, + "orderPlaced": { + "thanksForOrder": "Thanks for your order, {name}!", + "thanksForOrderAnonymous": "Thanks for your order!", + "orderNumber": "Order #{number}", + "emailConfirmation": "You will receive an email confirmation shortly.", + "orderItems": "Order Items", + "contactInformation": "Contact Information", + "shippingAddress": "Shipping Address", + "billingAddress": "Billing Address", + "orderNotFound": "Order not found", + "failedToLoad": "Failed to load order details." + }, + "coupon": { + "placeholder": "Gift card or discount code", + "apply": "Apply", + "remove": "Remove", + "invalidCode": "Invalid coupon code", + "applyFailed": "Could not apply coupon. Please try again.", + "failedToRemove": "Failed to remove coupon code", + "removeCoupon": "Remove coupon {code}", + "applying": "Applying...", + "removing": "Removing..." + }, + "checkoutLayout": { + "backToStore": "Back to store", + "showOrderSummary": "Show order summary", + "hideOrderSummary": "Hide order summary", + "allRightsReserved": "© {year} {storeName}. All rights reserved." + }, + "profile": { + "profile": "Profile", + "firstName": "First Name", + "lastName": "Last Name", + "emailAddress": "Email Address", + "saveChanges": "Save Changes", + "saving": "Saving...", + "profileUpdated": "Profile updated successfully!", + "accountInformation": "Account Information", + "accountId": "Account ID", + "loadingProfile": "Loading profile...", + "failedToUpdate": "Failed to update profile" + }, + "creditCards": { + "paymentMethods": "Payment Methods", + "noCards": "No payment methods saved", + "noCardsDescription": "Payment methods are saved automatically when you make a purchase.", + "expires": "Expires", + "default": "Default", + "removing": "Removing...", + "secureInfo": "Your payment information is securely stored.", + "deleteConfirm": "Are you sure you want to remove this card?", + "cardMaskedLabel": "{label} ending in {digits}, expires {month}/{year}" + }, + "giftCards": { + "giftCards": "Gift Cards", + "noGiftCards": "No gift cards", + "noGiftCardsDescription": "You don't have any gift cards yet.", + "active": "Active", + "partiallyUsed": "Partially Used", + "fullyRedeemed": "Fully Redeemed", + "canceled": "Canceled", + "expired": "Expired", + "noExpiration": "No expiration", + "remaining": "remaining", + "usedAmount": "Used: {amount}", + "totalAmountWithValue": "Total: {amount}", + "addedOnDate": "Added on {date}", + "fullyRedeemedOnDate": "Fully redeemed on {date}", + "activeGiftCards": "Active Gift Cards", + "expiredRedeemed": "Expired / Redeemed", + "copy": "Copy", + "copied": "Copied!", + "copyCodeToClipboard": "Copy code to clipboard", + "expiresOn": "Expires {date}", + "percentUsed": "{percent}% used", + "giftCardsHelpText": "Gift cards can be used during checkout to pay for your orders. The remaining balance will be saved for future purchases." + } +} diff --git a/messages/pl.json b/messages/pl.json new file mode 100644 index 00000000..424ff17e --- /dev/null +++ b/messages/pl.json @@ -0,0 +1,382 @@ +{ + "common": { + "subtotal": "Suma częściowa", + "shipping": "Wysyłka", + "tax": "Podatek", + "discount": "Rabat", + "total": "Suma", + "adjustments": "Korekty", + "edit": "Edytuj", + "remove": "Usuń", + "cancel": "Anuluj", + "back": "Wstecz", + "continue": "Kontynuuj", + "loading": "Ładowanie...", + "apply": "Zastosuj", + "saving": "Zapisywanie...", + "processing": "Przetwarzanie...", + "continueShopping": "Kontynuuj zakupy", + "orderSummary": "Podsumowanie zamówienia", + "selectCountry": "Wybierz kraj", + "myAccount": "Moje konto" + }, + "header": { + "categories": "Kategorie", + "openCart": "Otwórz koszyk", + "account": "Konto" + }, + "footer": { + "description": "Nowoczesny sklep internetowy oparty na Spree Commerce i Next.js.", + "shop": "Sklep", + "allProducts": "Wszystkie produkty", + "categories": "Kategorie", + "account": "Konto", + "myAccount": "Moje konto", + "orderHistory": "Historia zamówień", + "cart": "Koszyk", + "copyright": "© {year} {storeName}. Zasilany przez Spree Commerce." + }, + "home": { + "welcome": "Witaj w Sklepie Spree", + "heroDescription": "Odkryj niesamowite produkty dzięki nowoczesnemu sklepowi internetowemu opartemu na Spree Commerce.", + "shopNow": "Kup teraz", + "browseCategories": "Przeglądaj kategorie", + "featuredProducts": "Polecane produkty", + "viewAll": "Zobacz wszystkie", + "qualityProducts": "Produkty wysokiej jakości", + "qualityDescription": "Starannie wyselekcjonowana kolekcja najlepszych produktów.", + "fastShipping": "Szybka wysyłka", + "shippingDescription": "Szybka i niezawodna dostawa pod Twoje drzwi.", + "support": "Wsparcie 24/7", + "supportDescription": "Nasz zespół jest do Twojej dyspozycji o każdej porze." + }, + "cart": { + "cart": "Koszyk", + "closeCart": "Zamknij koszyk", + "emptyCart": "Twój koszyk jest pusty", + "emptyCartDescription": "Wygląda na to, że nie dodałeś jeszcze nic do koszyka.", + "removeItem": "Usuń produkt", + "decreaseQuantity": "Zmniejsz ilość", + "increaseQuantity": "Zwiększ ilość", + "shippingNote": "Koszty wysyłki i podatki zostaną obliczone przy kasie", + "checkout": "Do kasy", + "viewCart": "Zobacz koszyk", + "shoppingCart": "Koszyk zakupowy", + "proceedToCheckout": "Przejdź do kasy", + "itemCount": "{count, plural, one {# produkt} few {# produkty} many {# produktów} other {# produktów}}", + "shippingCalculatedAtCheckout": "Obliczona przy kasie" + }, + "products": { + "allProducts": "Wszystkie produkty", + "browseCollection": "Przeglądaj naszą pełną kolekcję", + "searchResultsFor": "Wyniki wyszukiwania dla \"{query}\"", + "productsFound": "{count, plural, one {# produkt} few {# produkty} other {# produktów}}", + "noProductsFound": "Nie znaleziono produktów", + "tryAdjustingFilters": "Spróbuj zmienić filtry", + "noMatchingProducts": "Nie znaleźliśmy produktów pasujących do \"{query}\"", + "showingProducts": "Wyświetlanie {shown} z {total} produktów", + "loadingMore": "Ładowanie kolejnych...", + "noMoreProducts": "Brak kolejnych produktów", + "filters": "Filtry", + "sortBy": "Sortuj wg", + "resetFilters": "Resetuj filtry", + "price": "Cena", + "availability": "Dostępność", + "inStock": "Dostępny", + "outOfStock": "Niedostępny", + "min": "Min", + "max": "Maks", + "range": "Zakres: {min} - {max}", + "clear": "Wyczyść", + "productCount": "{count, plural, =0 {0 produktów} one {# produkt} few {# produkty} many {# produktów} other {# produktów}}", + "sale": "Wyprzedaż", + "outOfStockVariant": "(Niedostępny)", + "addToCart": "Dodaj do koszyka", + "adding": "Dodawanie...", + "description": "Opis", + "details": "Szczegóły", + "sku": "SKU", + "options": "Opcje", + "search": "Szukaj...", + "searching": "Wyszukiwanie...", + "viewAllResultsFor": "Zobacz wszystkie wyniki dla \"{query}\"", + "manual": "Ręcznie", + "bestSelling": "Bestsellery", + "priceLowHigh": "Cena (rosnąco)", + "priceHighLow": "Cena (malejąco)", + "newest": "Najnowsze", + "oldest": "Najstarsze", + "nameAZ": "Nazwa (A-Z)", + "nameZA": "Nazwa (Z-A)", + "sortUnknown": "Inne", + "categories": "Kategorie", + "noCategoriesFound": "Nie znaleziono kategorii.", + "noCategoriesInGroup": "Brak kategorii w tej grupie.", + "subcategories": "{count, plural, =0 {0 podkategorii} one {# podkategoria} few {# podkategorie} many {# podkategorii} other {# podkategorii}}", + "noImageAvailable": "Brak zdjęcia", + "clickToZoom": "Kliknij aby powiększyć", + "noProductsMatchingFilters": "Nie znaleziono produktów pasujących do wybranych filtrów.", + "decreaseQuantity": "Zmniejsz ilość", + "increaseQuantity": "Zwiększ ilość", + "carouselPrev": "Poprzednie produkty", + "carouselNext": "Następne produkty", + "lightboxClose": "Zamknij podgląd", + "lightboxPrev": "Poprzednie zdjęcie", + "lightboxNext": "Następne zdjęcie", + "subcategoriesHeading": "Podkategorie", + "sort": "Sortuj", + "clearAll": "Wyczyść wszystko", + "clearAllFiltersCount": "Wyczyść wszystkie filtry ({count})", + "showResults": "Pokaż wyniki", + "closeFilters": "Zamknij filtry", + "anyAvailability": "Dowolna dostępność", + "customPrice": "Niestandardowa", + "priceUnder": "Poniżej {price}", + "priceAbove": "{price}+", + "priceRangeBucket": "{min} – {max}" + }, + "navigation": { + "home": "Strona główna", + "categories": "Kategorie", + "breadcrumb": "Ścieżka nawigacji" + }, + "checkout": { + "checkout": "Kasa", + "orderNumber": "Zamówienie #{number}", + "checkoutError": "Błąd kasy", + "returnToCart": "Wróć do koszyka", + "emptyCart": "Twój koszyk jest pusty", + "emptyCartDescription": "Dodaj produkty do koszyka przed złożeniem zamówienia.", + "stepShipping": "Wysyłka", + "stepDelivery": "Dostawa", + "stepPayment": "Płatność", + "contactInformation": "Dane kontaktowe", + "emailAddress": "Adres e-mail", + "emailPlaceholder": "ty@przyklad.pl", + "usingAccountEmail": "Używasz adresu e-mail Twojego konta", + "signInPrompt": "Masz już konto?", + "signIn": "Zaloguj się", + "signInDescription": "aby uzyskać dostęp do zapisanych adresów i historii zamówień.", + "shippingAddress": "Adres wysyłki", + "continueToDelivery": "Przejdź do dostawy", + "shippingMethod": "Metoda wysyłki", + "loadingShippingOptions": "Ładowanie opcji wysyłki...", + "shipmentOf": "Przesyłka {current} z {total}", + "shipsFrom": "Wysyłka z {location}", + "phone": "Telefon: {phone}", + "billingAddress": "Adres rozliczeniowy", + "sameAsShipping": "Taki sam jak adres wysyłki", + "paymentMethod": "Metoda płatności", + "loadingPaymentForm": "Ładowanie formularza płatności...", + "addNewPaymentMethod": "Dodaj nową metodę płatności", + "endingIn": "kończąca się na", + "exp": "Ważna do", + "default": "Domyślna", + "noPaymentMethods": "Brak dostępnych metod płatności dla tego zamówienia.", + "payNow": "Zapłać teraz", + "failedToInitPayment": "Nie udało się zainicjować płatności. Spróbuj ponownie.", + "failedToCreateSession": "Nie udało się utworzyć sesji płatności.", + "paymentError": "Wystąpił błąd podczas płatności. Spróbuj ponownie.", + "orderNotFound": "Zamówienie nie zostało znalezione lub nie masz do niego dostępu.", + "failedToLoadCheckout": "Nie udało się załadować kasy. Spróbuj ponownie.", + "orderNotFoundAfterPayment": "Zamówienie nie zostało znalezione po płatności. Skontaktuj się z obsługą.", + "failedToSaveAddress": "Nie udało się zapisać adresu", + "failedToProceed": "Nie udało się przejść do następnego kroku", + "generalError": "Wystąpił błąd. Spróbuj ponownie.", + "failedToSelectRate": "Nie udało się wybrać stawki wysyłki", + "failedToSaveBilling": "Nie udało się zapisać adresu rozliczeniowego", + "failedToSaveBillingRetry": "Nie udało się zapisać adresu rozliczeniowego. Spróbuj ponownie.", + "failedToCompleteOrder": "Nie udało się złożyć zamówienia", + "stripeNotLoaded": "Stripe nie został jeszcze załadowany", + "paymentProcessingError": "Wystąpił błąd podczas przetwarzania płatności.", + "failedToCompletePaymentSession": "Nie udało się zakończyć sesji płatności" + }, + "address": { + "firstName": "Imię", + "lastName": "Nazwisko", + "company": "Firma (opcjonalnie)", + "address": "Adres", + "streetAddress": "Ulica i numer", + "apartment": "Mieszkanie, lokal itp. (opcjonalnie)", + "city": "Miasto", + "country": "Kraj", + "selectCountry": "Wybierz kraj", + "stateProvince": "Województwo", + "selectState": "Wybierz województwo", + "stateOrProvince": "Województwo", + "zipCode": "Kod pocztowy", + "phone": "Telefon (opcjonalnie)", + "selectAnAddress": "Wybierz adres", + "useDifferentAddress": "Użyj innego adresu", + "editAddress": "Edytuj adres", + "addNewAddress": "Dodaj nowy adres", + "saveAddress": "Zapisz adres", + "failedToSave": "Nie udało się zapisać adresu" + }, + "account": { + "myAccount": "Moje konto", + "signInDescription": "Zaloguj się, aby uzyskać dostęp do konta i historii zamówień.", + "email": "E-mail", + "password": "Hasło", + "signIn": "Zaloguj się", + "signingIn": "Logowanie...", + "invalidCredentials": "Nieprawidłowy e-mail lub hasło", + "dontHaveAccount": "Nie masz konta?", + "signUp": "Zarejestruj się", + "accountOverview": "Przegląd konta", + "orderHistory": "Historia zamówień", + "orderHistoryDescription": "Przeglądaj poprzednie zamówienia i śledź przesyłki", + "addresses": "Adresy", + "addressesDescription": "Zarządzaj adresami wysyłki i rozliczeń", + "paymentMethods": "Metody płatności", + "paymentMethodsDescription": "Zarządzaj zapisanymi kartami płatniczymi", + "profile": "Profil", + "profileDescription": "Zaktualizuj swoje dane osobowe", + "signOut": "Wyloguj się", + "overview": "Przegląd", + "orders": "Zamówienia", + "giftCards": "Karty podarunkowe", + "addNewAddress": "Dodaj nowy adres", + "noAddresses": "Brak zapisanych adresów", + "noAddressesDescription": "Adresy dodane podczas składania zamówienia pojawią się tutaj.", + "defaultAddress": "Domyślny", + "deleteConfirm": "Czy na pewno chcesz usunąć ten adres?", + "setAsDefault": "Ustaw jako domyślny", + "showPassword": "Pokaż hasło", + "hidePassword": "Ukryj hasło" + }, + "register": { + "createAccount": "Utwórz konto", + "signUpDescription": "Zarejestruj się, aby rozpocząć zakupy.", + "confirmPassword": "Potwierdź hasło", + "creatingAccount": "Tworzenie konta...", + "passwordsDontMatch": "Hasła nie są takie same", + "passwordTooShort": "Hasło musi mieć co najmniej 6 znaków", + "registrationFailed": "Rejestracja nie powiodła się. Spróbuj ponownie.", + "alreadyHaveAccount": "Masz już konto?", + "signIn": "Zaloguj się" + }, + "orders": { + "orderHistory": "Historia zamówień", + "noOrders": "Brak zamówień", + "noOrdersDescription": "Gdy złożysz zamówienie, pojawi się tutaj.", + "startShopping": "Zacznij zakupy", + "order": "Zamówienie", + "date": "Data", + "status": "Status", + "actions": "Akcje", + "view": "Zobacz", + "orderNotFound": "Zamówienie nie zostało znalezione", + "backToOrders": "Wróć do zamówień", + "placedOn": "Złożone {date}", + "orderItems": "Pozycje zamówienia", + "qty": "Ilość: {quantity}", + "orderAgain": "Zamów ponownie", + "deliveryAddress": "Adres dostawy", + "shippingMethod": "Metoda wysyłki", + "shippedFrom": "Wysłane z {location}", + "trackItems": "Śledź przesyłkę", + "shipmentCanceled": "Ta przesyłka została anulowana.", + "noTrackingInfo": "Brak informacji o śledzeniu.", + "paymentInformation": "Informacje o płatności", + "cardEndingIn": "{label} kończąca się na {digits}", + "cardExpires": "Ważna do {month}/{year}", + "storeCreditApplied": "Zastosowano {amount} — {remaining} pozostało", + "billingAddress": "Adres rozliczeniowy", + "orderNotFoundDescription": "Zamówienie, którego szukasz, nie istnieje.", + "storeCredit": "Kredyt sklepowy", + "specialInstructions": "Specjalne instrukcje", + "payment": "Płatność", + "shipment": "Przesyłka", + "shipped": "Wysłano", + "partiallyShipped": "Częściowo wysłano", + "delivered": "Dostarczono", + "ready": "Gotowe", + "pending": "Oczekujące", + "canceled": "Anulowane", + "backorder": "Zamówienie wsteczne", + "paid": "Opłacone", + "balanceDue": "Do zapłaty", + "creditOwed": "Należny kredyt", + "failed": "Nieudane", + "void": "Unieważnione", + "notAvailable": "Brak" + }, + "orderPlaced": { + "thanksForOrder": "Dziękujemy za zamówienie, {name}!", + "thanksForOrderAnonymous": "Dziękujemy za zamówienie!", + "orderNumber": "Zamówienie #{number}", + "emailConfirmation": "Wkrótce otrzymasz potwierdzenie e-mailem.", + "orderItems": "Pozycje zamówienia", + "contactInformation": "Dane kontaktowe", + "shippingAddress": "Adres wysyłki", + "billingAddress": "Adres rozliczeniowy", + "orderNotFound": "Zamówienie nie zostało znalezione", + "failedToLoad": "Nie udało się załadować szczegółów zamówienia." + }, + "coupon": { + "placeholder": "Karta podarunkowa lub kod rabatowy", + "apply": "Zastosuj", + "remove": "Usuń", + "invalidCode": "Nieprawidłowy kod kuponu", + "applyFailed": "Nie udało się zastosować kuponu. Spróbuj ponownie.", + "failedToRemove": "Nie udało się usunąć kodu kuponu", + "removeCoupon": "Usuń kupon {code}", + "applying": "Stosowanie...", + "removing": "Usuwanie..." + }, + "checkoutLayout": { + "backToStore": "Wróć do sklepu", + "showOrderSummary": "Pokaż podsumowanie zamówienia", + "hideOrderSummary": "Ukryj podsumowanie zamówienia", + "allRightsReserved": "© {year} {storeName}. Wszelkie prawa zastrzeżone." + }, + "profile": { + "profile": "Profil", + "firstName": "Imię", + "lastName": "Nazwisko", + "emailAddress": "Adres e-mail", + "saveChanges": "Zapisz zmiany", + "saving": "Zapisywanie...", + "profileUpdated": "Profil został zaktualizowany!", + "accountInformation": "Informacje o koncie", + "accountId": "ID konta", + "loadingProfile": "Ładowanie profilu...", + "failedToUpdate": "Nie udało się zaktualizować profilu" + }, + "creditCards": { + "paymentMethods": "Metody płatności", + "noCards": "Brak zapisanych metod płatności", + "noCardsDescription": "Metody płatności są zapisywane automatycznie podczas zakupów.", + "expires": "Ważna do", + "default": "Domyślna", + "removing": "Usuwanie...", + "secureInfo": "Twoje dane płatnicze są bezpiecznie przechowywane.", + "deleteConfirm": "Czy na pewno chcesz usunąć tę kartę?", + "cardMaskedLabel": "{label} kończąca się na {digits}, ważna do {month}/{year}" + }, + "giftCards": { + "giftCards": "Karty podarunkowe", + "noGiftCards": "Brak kart podarunkowych", + "noGiftCardsDescription": "Nie masz jeszcze żadnych kart podarunkowych.", + "active": "Aktywna", + "partiallyUsed": "Częściowo wykorzystana", + "fullyRedeemed": "W pełni wykorzystana", + "canceled": "Anulowana", + "expired": "Wygasła", + "noExpiration": "Bez daty ważności", + "remaining": "pozostało", + "usedAmount": "Wykorzystano: {amount}", + "totalAmountWithValue": "Łącznie: {amount}", + "addedOnDate": "Dodano {date}", + "fullyRedeemedOnDate": "W pełni wykorzystano {date}", + "activeGiftCards": "Aktywne karty podarunkowe", + "expiredRedeemed": "Wygasłe / Wykorzystane", + "copy": "Kopiuj", + "copied": "Skopiowano!", + "copyCodeToClipboard": "Skopiuj kod do schowka", + "expiresOn": "Wygasa {date}", + "percentUsed": "{percent}% wykorzystano", + "giftCardsHelpText": "Karty podarunkowe mogą być używane podczas składania zamówienia do opłacenia zakupów. Pozostałe saldo zostanie zapisane na przyszłe zakupy." + } +} diff --git a/next.config.ts b/next.config.ts index 84b3a538..a8d5c92e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,8 @@ import { withSentryConfig } from "@sentry/nextjs"; import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); const nextConfig: NextConfig = { env: { @@ -43,8 +46,10 @@ const nextConfig: NextConfig = { }, }; +const configWithIntl = withNextIntl(nextConfig); + export default process.env.SENTRY_DSN - ? withSentryConfig(nextConfig, { + ? withSentryConfig(configWithIntl, { org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, @@ -62,4 +67,4 @@ export default process.env.SENTRY_DSN // Disables the Sentry SDK build-time telemetry telemetry: false, }) - : nextConfig; + : configWithIntl; diff --git a/package-lock.json b/package-lock.json index f299b43f..a9886bbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.577.0", "next": "^16", + "next-intl": "^4.8.3", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19", @@ -180,6 +181,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -863,6 +865,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -903,6 +906,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1649,6 +1653,58 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz", + "integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/intl-localematcher": "0.8.1", + "decimal.js": "^10.6.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", + "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz", + "integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/icu-skeleton-parser": "2.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz", + "integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", + "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -2244,7 +2300,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2492,6 +2547,7 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2588,6 +2644,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2609,6 +2666,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz", "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2621,6 +2679,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -3044,6 +3103,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -3060,6 +3120,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz", "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1", @@ -3077,6 +3138,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -3096,6 +3158,301 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@prisma/instrumentation": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.2.0.tgz", @@ -5015,6 +5372,12 @@ "win32" ] }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -5478,74 +5841,242 @@ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@spree/next": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@spree/next/-/next-0.9.0.tgz", + "integrity": "sha512-Qa0+OW3xfRbookWTwev/ZgzUG8xNvrnAtVyoHxEbDy3XHZcWmIw6AMd/ph6QPv7uzx47Y6NhGFwZ4ljkqEn9DA==", + "license": "MIT", + "peerDependencies": { + "@spree/sdk": ">=0.9.0", + "next": ">=15.0.0", + "react": ">=19.0.0" + } + }, + "node_modules/@spree/sdk": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@spree/sdk/-/sdk-0.9.0.tgz", + "integrity": "sha512-RWYNyhsVYrevubpeXt5NfWJrc9xr0GG9zj2TrxU5fT6PsZEh4VK8DYlgfG9C/q2Vp6a9p99QiZ5z09bZ9YlW5g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typescript": ">=4.7.0", + "zod": ">=3.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stripe/react-stripe-js": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.6.0.tgz", + "integrity": "sha512-tucu/vTGc+5NXbo2pUiaVjA4ENdRBET8qGS00BM4BAU8J4Pi3eY6BHollsP2+VSuzzlvXwMg0it3ZLhbCj2fPg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=8.0.0 <9.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.8.0.tgz", + "integrity": "sha512-NNYuyW8qmLjyHnpyFgs/23wUrjB8k0xN9YIZFOMLewCa/pIkIji9e9aY/EgdNryEDDRptc6TcPIHRvG1R0ClFw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.16" + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@spree/next": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@spree/next/-/next-0.9.0.tgz", - "integrity": "sha512-Qa0+OW3xfRbookWTwev/ZgzUG8xNvrnAtVyoHxEbDy3XHZcWmIw6AMd/ph6QPv7uzx47Y6NhGFwZ4ljkqEn9DA==", - "license": "MIT", - "peerDependencies": { - "@spree/sdk": ">=0.9.0", - "next": ">=15.0.0", - "react": ">=19.0.0" + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@spree/sdk": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@spree/sdk/-/sdk-0.9.0.tgz", - "integrity": "sha512-RWYNyhsVYrevubpeXt5NfWJrc9xr0GG9zj2TrxU5fT6PsZEh4VK8DYlgfG9C/q2Vp6a9p99QiZ5z09bZ9YlW5g==", - "license": "MIT", + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": ">=4.7.0", - "zod": ">=3.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "node": ">=10" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@stripe/react-stripe-js": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.6.0.tgz", - "integrity": "sha512-tucu/vTGc+5NXbo2pUiaVjA4ENdRBET8qGS00BM4BAU8J4Pi3eY6BHollsP2+VSuzzlvXwMg0it3ZLhbCj2fPg==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "@stripe/stripe-js": ">=8.0.0 <9.0.0", - "react": ">=16.8.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@stripe/stripe-js": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.8.0.tgz", - "integrity": "sha512-NNYuyW8qmLjyHnpyFgs/23wUrjB8k0xN9YIZFOMLewCa/pIkIji9e9aY/EgdNryEDDRptc6TcPIHRvG1R0ClFw==", - "license": "MIT", + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.16" + "node": ">=10" } }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -5555,6 +6086,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -5996,8 +6536,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6076,7 +6615,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6087,7 +6625,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -6103,8 +6640,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/mysql": { "version": "2.15.27", @@ -6150,6 +6686,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6160,6 +6697,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6332,7 +6870,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -6342,29 +6879,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6375,15 +6908,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6396,7 +6927,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6406,7 +6936,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6415,15 +6944,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6440,7 +6967,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -6454,7 +6980,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6467,7 +6992,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6482,7 +7006,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -6492,15 +7015,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/accepts": { "version": "2.0.0", @@ -6545,6 +7066,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6566,7 +7088,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -6591,6 +7112,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6607,7 +7129,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -6625,7 +7146,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -6648,7 +7168,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6813,6 +7332,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6831,8 +7351,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bundle-name": { "version": "4.1.0", @@ -6943,7 +7462,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -7312,7 +7830,6 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, "license": "MIT" }, "node_modules/dedent": { @@ -7401,7 +7918,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7427,8 +7943,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dotenv": { "version": "16.6.1", @@ -7630,7 +8145,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -7657,7 +8171,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -7670,7 +8183,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -7680,7 +8192,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -7705,7 +8216,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -8226,8 +8736,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/gopd": { "version": "1.2.0", @@ -8261,7 +8770,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -8301,6 +8809,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -8400,6 +8909,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icu-minify": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz", + "integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8453,6 +8977,18 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/intl-messageformat": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz", + "integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/icu-messageformat-parser": "3.5.1", + "tslib": "^2.8.1" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -8687,7 +9223,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -9297,7 +9832,6 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -9385,7 +9919,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9481,7 +10014,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -9491,7 +10023,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9687,14 +10218,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/next": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", @@ -9743,6 +10274,82 @@ } } }, + "node_modules/next-intl": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz", + "integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.8.3", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.8.3", + "po-parser": "^2.1.1", + "use-intl": "^4.8.3" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz", + "integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -9781,6 +10388,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -10236,6 +10849,12 @@ "node": ">=16.20.0" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -10334,7 +10953,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -10577,6 +11195,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10586,6 +11205,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10598,8 +11218,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.18.0", @@ -10797,6 +11416,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10927,7 +11547,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -11357,7 +11976,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -11539,7 +12157,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11623,7 +12240,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -11642,7 +12258,6 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -11675,8 +12290,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/third-party-capital": { "version": "1.0.20", @@ -11890,6 +12504,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12004,6 +12619,27 @@ } } }, + "node_modules/use-intl": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz", + "integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.8.3", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -12078,6 +12714,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12243,7 +12880,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -12276,7 +12912,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -12325,7 +12960,6 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -12334,8 +12968,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/whatwg-mimetype": { "version": "5.0.0", @@ -12618,6 +13251,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 7aae6d40..996d7a87 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format": "biome format . --write", "check": "biome check .", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "check:locales": "npx tsx scripts/check-locale-parity.ts" }, "dependencies": { "@next/third-parties": "^16.1.6", @@ -23,6 +24,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.577.0", "next": "^16", + "next-intl": "^4.8.3", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19", diff --git a/scripts/check-locale-parity.ts b/scripts/check-locale-parity.ts new file mode 100644 index 00000000..a69387c0 --- /dev/null +++ b/scripts/check-locale-parity.ts @@ -0,0 +1,79 @@ +/** + * Checks that all locale JSON files have the same set of keys as en.json. + * Run: npx tsx scripts/check-locale-parity.ts + */ +import fs from "node:fs"; +import path from "node:path"; + +const MESSAGES_DIR = path.resolve(__dirname, "../messages"); +const BASE_LOCALE = "en"; + +function getNestedKeys(obj: Record, prefix = ""): string[] { + const keys: string[] = []; + for (const key of Object.keys(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + keys.push(...getNestedKeys(value as Record, fullKey)); + } else { + keys.push(fullKey); + } + } + return keys; +} + +const baseFile = path.join(MESSAGES_DIR, `${BASE_LOCALE}.json`); +let baseMessages: Record; +try { + baseMessages = JSON.parse(fs.readFileSync(baseFile, "utf-8")); +} catch (err) { + console.error(`Failed to parse base locale file ${baseFile}:`, err); + process.exit(1); +} +const baseKeys = new Set(getNestedKeys(baseMessages)); + +const localeFiles = fs + .readdirSync(MESSAGES_DIR) + .filter((f) => f.endsWith(".json") && f !== `${BASE_LOCALE}.json`); + +let hasError = false; + +for (const file of localeFiles) { + const locale = file.replace(".json", ""); + let messages: Record; + try { + messages = JSON.parse( + fs.readFileSync(path.join(MESSAGES_DIR, file), "utf-8"), + ); + } catch (err) { + console.error(`Failed to parse locale file ${file}:`, err); + hasError = true; + continue; + } + const localeKeys = new Set(getNestedKeys(messages)); + + const missing = [...baseKeys].filter((k) => !localeKeys.has(k)); + const extra = [...localeKeys].filter((k) => !baseKeys.has(k)); + + if (missing.length > 0) { + console.error(`[${locale}] Missing ${missing.length} key(s):`); + for (const k of missing) console.error(` - ${k}`); + hasError = true; + } + + if (extra.length > 0) { + console.error(`[${locale}] Extra ${extra.length} key(s):`); + for (const k of extra) console.error(` + ${k}`); + hasError = true; + } + + if (missing.length === 0 && extra.length === 0) { + console.log(`[${locale}] OK — all keys match ${BASE_LOCALE}.json`); + } +} + +if (hasError) { + process.exit(1); +} else { + console.log("\nAll locale files are in sync."); +} diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx index 2975be45..2af87db0 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx @@ -10,6 +10,7 @@ import type { import { CircleAlert } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { use, useCallback, useEffect, useRef, useState } from "react"; import { AddressStep } from "@/components/checkout/AddressStep"; import { CouponCode } from "@/components/checkout/CouponCode"; @@ -41,11 +42,7 @@ import { } from "@/lib/data/payment"; import { extractBasePath } from "@/lib/utils/path"; -const CHECKOUT_STEPS = [ - { id: "address", label: "Shipping" }, - { id: "delivery", label: "Delivery" }, - { id: "payment", label: "Payment" }, -]; +const CHECKOUT_STEP_IDS = ["address", "delivery", "payment"] as const; interface CheckoutPageProps { params: Promise<{ @@ -90,6 +87,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const pathname = usePathname(); const basePath = extractBasePath(pathname); const { setSummaryContent } = useCheckout(); + const t = useTranslations("checkout"); + + const stepLabels: Record = { + address: t("stepShipping"), + delivery: t("stepDelivery"), + payment: t("stepPayment"), + }; const [order, setOrder] = useState(null); const [shipments, setShipments] = useState([]); @@ -187,7 +191,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { : { data: [] as Country[] }; if (!orderData) { - setError("Order not found or you don't have access to it."); + setError(t("orderNotFound")); setLoading(false); return; } @@ -220,12 +224,12 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { return orderData; } catch { - setError("Failed to load checkout. Please try again."); + setError(t("failedToLoadCheckout")); return null; } finally { setLoading(false); } - }, [orderId, urlCountry, basePath, router]); + }, [orderId, urlCountry, basePath, router, t]); useEffect(() => { loadOrder(); @@ -479,14 +483,14 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { return (

- Checkout Error + {t("checkoutError")}

{error}

- Return to Cart + {t("returnToCart")}
); @@ -499,22 +503,23 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { return (

- Your Cart is Empty + {t("emptyCart")}

-

- Add some items to your cart before checking out. -

+

{t("emptyCartDescription")}

- Continue Shopping + {t("returnToCart")}
); } - const steps = CHECKOUT_STEPS; + const steps = CHECKOUT_STEP_IDS.map((id) => ({ + id, + label: stepLabels[id] || id.charAt(0).toUpperCase() + id.slice(1), + })); const currentStepIndex = steps.findIndex((s) => s.id === currentStep); const previousStepId = currentStepIndex > 0 ? steps[currentStepIndex - 1].id : undefined; @@ -523,7 +528,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { <> {/* Header */}
-

Checkout

+

{t("checkout")}

{/* Step indicator */} diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index ba1f59d3..dfbfa292 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -4,15 +4,18 @@ import { ArrowLeft, ChevronDown, ShoppingBag } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { CheckoutProvider, CheckoutSummary } from "@/contexts/CheckoutContext"; +import { getStoreName } from "@/lib/seo"; import { extractBasePath } from "@/lib/utils/path"; -const storeName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; +const storeName = getStoreName(); function CheckoutHeader() { const pathname = usePathname(); const basePath = extractBasePath(pathname); + const t = useTranslations("checkoutLayout"); return (
@@ -28,22 +31,22 @@ function CheckoutHeader() {
); } function CheckoutFooter() { - const currentYear = new Date().getFullYear(); + const t = useTranslations("checkoutLayout"); return (

- © {currentYear} {storeName}. All rights reserved. + {t("allRightsReserved", { year: new Date().getFullYear(), storeName })}

); @@ -51,6 +54,7 @@ function CheckoutFooter() { function MobileSummaryToggle() { const [isOpen, setIsOpen] = useState(false); + const t = useTranslations("checkoutLayout"); return (
@@ -61,7 +65,7 @@ function MobileSummaryToggle() { > - {isOpen ? "Hide order summary" : "Show order summary"} + {isOpen ? t("hideOrderSummary") : t("showOrderSummary")} (null); const [loading, setLoading] = useState(true); @@ -72,14 +75,14 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { // Analytics failure must not break the order confirmation UX } } else { - setError("Order not found."); + setError(t("orderNotFound")); } setLoading(false); } } catch { if (!cancelled) { loadedRef.current = true; - setError("Failed to load order details."); + setError(t("failedToLoad")); setLoading(false); } } @@ -90,7 +93,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { return () => { cancelled = true; }; - }, [orderId, searchParams]); + }, [orderId, searchParams, t]); if (loading) { return ( @@ -107,10 +110,10 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { return (

- {error || "Order not found"} + {error || t("orderNotFound")}

); @@ -125,19 +128,22 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) {

- Thanks for your order - {customerName ? `, ${customerName.split(" ")[0]}` : ""}! + {customerName + ? t("thanksForOrder", { name: customerName.split(" ")[0] }) + : t("thanksForOrderAnonymous")}

-

Order #{order.number}

-

- You will receive an email confirmation shortly. +

+ {t("orderNumber", { number: order.number })}

+

{t("emailConfirmation")}

{/* Order Items */}
-

Order Items

+

+ {t("orderItems")} +

    {order.items?.map((item) => ( @@ -170,27 +176,27 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { {/* Totals */}
    - Subtotal + {tc("subtotal")} {order.display_item_total}
    - Shipping + {tc("shipping")} {order.display_ship_total}
    {order.promo_total && Number.parseFloat(order.promo_total) !== 0 && (
    - Discount + {tc("discount")} {order.display_promo_total}
    )}
    - Tax + {tc("tax")} {order.display_tax_total}
    - Total + {tc("total")} {order.display_total} @@ -202,7 +208,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) {

    - Contact Information + {t("contactInformation")}

    @@ -214,7 +220,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { {order.ship_address && (

    - Shipping Address + {t("shippingAddress")}

    @@ -242,7 +248,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { {order.bill_address && (

    - Billing Address + {t("billingAddress")}

    @@ -270,7 +276,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { {/* Actions */}

    diff --git a/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx b/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx index 0d15ed45..823ad496 100644 --- a/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx @@ -2,6 +2,7 @@ import type { Address, AddressParams, Country } from "@spree/sdk"; import { MapPin, Plus } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; import { AddressEditModal } from "@/components/checkout/AddressEditModal"; import { @@ -33,6 +34,8 @@ function AddressCard({ onEdit: () => void; onDelete: () => void; }) { + const t = useTranslations("account"); + const tc = useTranslations("common"); const [deleting, setDeleting] = useState(false); const handleDelete = async () => { @@ -63,20 +66,19 @@ function AddressCard({
    Delete address? - This will permanently delete this address. This action cannot - be undone. + {t("deleteConfirm")} @@ -94,6 +96,7 @@ function AddressCard({ } export default function AddressesPage() { + const t = useTranslations("account"); const [addresses, setAddresses] = useState([]); const [countries, setCountries] = useState([]); const [loading, setLoading] = useState(true); @@ -167,7 +170,9 @@ export default function AddressesPage() { if (loading) { return (
    -

    Addresses

    +

    + {t("addresses")} +

    {[1, 2].map((i) => (
    -

    Addresses

    +

    {t("addresses")}

    @@ -197,12 +202,10 @@ export default function AddressesPage() {

    - No addresses saved + {t("noAddresses")}

    -

    - Add an address for faster checkout. -

    - +

    {t("noAddressesDescription")}

    +
    ) : (
    diff --git a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx index cd940e9c..5cf10c9c 100644 --- a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx @@ -2,6 +2,7 @@ import type { CreditCard as SpreeCreditCard } from "@spree/sdk"; import { CreditCard, Lock } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; import { PaymentIcon } from "react-svg-credit-card-payment-icons"; import { @@ -26,6 +27,8 @@ function CreditCardItem({ card: SpreeCreditCard; onDelete: () => void; }) { + const t = useTranslations("creditCards"); + const tc = useTranslations("common"); const [deleting, setDeleting] = useState(false); const handleDelete = async () => { @@ -51,7 +54,7 @@ function CreditCardItem({ {getCardLabel(card.cc_type)} ending in {card.last_digits}

    - Expires {String(card.month).padStart(2, "0")}/{card.year} + {t("expires")} {String(card.month).padStart(2, "0")}/{card.year}

    {card.name && (

    {card.name}

    @@ -61,28 +64,26 @@ function CreditCardItem({
    {card.default && ( - Default + {t("default")} )} Remove payment method? - This will remove the {getCardLabel(card.cc_type)} ending in{" "} - {card.last_digits} from your account. This action cannot be - undone. + {t("deleteConfirm")} Cancel - Remove + {tc("remove")} @@ -94,6 +95,7 @@ function CreditCardItem({ } export default function CreditCardsPage() { + const t = useTranslations("creditCards"); const [cards, setCards] = useState([]); const [loading, setLoading] = useState(true); @@ -125,7 +127,7 @@ export default function CreditCardsPage() { return (

    - Payment Methods + {t("paymentMethods")}

    {[1, 2].map((i) => ( @@ -144,17 +146,17 @@ export default function CreditCardsPage() { return (
    -

    Payment Methods

    +

    + {t("paymentMethods")} +

    {cards.length === 0 ? (

    - No payment methods saved + {t("noCards")}

    -

    - Payment methods are saved automatically when you make a purchase. -

    +

    {t("noCardsDescription")}

    ) : (
    @@ -171,8 +173,7 @@ export default function CreditCardsPage() {

    - Your payment information is securely stored and encrypted. We never - store your full card number. + {t("secureInfo")}

    diff --git a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx index 2de995a4..6d2a019d 100644 --- a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx @@ -2,6 +2,7 @@ import type { GiftCard } from "@spree/sdk"; import { Check, ClipboardCopy, Gift, Info } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { getGiftCards } from "@/lib/data/gift-cards"; @@ -22,25 +23,8 @@ function getStateColor(state: string, expired: boolean): string { } } -function getStateLabel(state: string): string { - switch (state) { - case "active": - return "Active"; - case "partially_redeemed": - return "Partially Used"; - case "redeemed": - return "Fully Redeemed"; - case "canceled": - return "Canceled"; - case "expired": - return "Expired"; - default: - return state; - } -} - function formatDate(dateString: string | null): string { - if (!dateString) return "No expiration"; + if (!dateString) return ""; const date = new Date(dateString); return date.toLocaleDateString("en-US", { year: "numeric", @@ -50,6 +34,7 @@ function formatDate(dateString: string | null): string { } function CopyButton({ code }: { code: string }) { + const t = useTranslations("giftCards"); const [copied, setCopied] = useState(false); const handleCopy = async () => { @@ -80,12 +65,12 @@ function CopyButton({ code }: { code: string }) { {copied ? ( <> - Copied! + {t("copied")} ) : ( <> - Copy + {t("copy")} )} @@ -93,11 +78,29 @@ function CopyButton({ code }: { code: string }) { } function GiftCardItem({ card }: { card: GiftCard }) { + const t = useTranslations("giftCards"); const usagePercentage = Number(card.amount) > 0 ? Math.round((Number(card.amount_used) / Number(card.amount)) * 100) : 0; + function getStateLabel(state: string): string { + switch (state) { + case "active": + return t("active"); + case "partially_redeemed": + return t("partiallyUsed"); + case "redeemed": + return t("fullyRedeemed"); + case "canceled": + return t("canceled"); + case "expired": + return t("expired"); + default: + return state; + } + } + return (
    @@ -115,15 +118,15 @@ function GiftCardItem({ card }: { card: GiftCard }) {

    {card.expires_at - ? `Expires ${formatDate(card.expires_at)}` - : "No expiration date"} + ? t("expiresOn", { date: formatDate(card.expires_at) }) + : t("noExpiration")}

    {card.display_amount_remaining}

    -

    remaining

    +

    {t("remaining")}

    @@ -131,9 +134,11 @@ function GiftCardItem({ card }: { card: GiftCard }) {
    - Used: {card.display_amount_used} + {t("usedAmount", { amount: card.display_amount_used })} + + + {t("totalAmountWithValue", { amount: card.display_amount })} - Total: {card.display_amount}
    -

    {usagePercentage}% used

    +

    + {t("percentUsed", { percent: usagePercentage })} +

    {/* Additional info */}
    - Added on {formatDate(card.created_at)} + {t("addedOnDate", { date: formatDate(card.created_at) })} {card.redeemed_at && ( Fully redeemed on {formatDate(card.redeemed_at)} )} @@ -160,6 +167,7 @@ function GiftCardItem({ card }: { card: GiftCard }) { } export default function GiftCardsPage() { + const t = useTranslations("giftCards"); const [cards, setCards] = useState([]); const [loading, setLoading] = useState(true); @@ -175,7 +183,9 @@ export default function GiftCardsPage() { if (loading) { return (
    -

    Gift Cards

    +

    + {t("giftCards")} +

    {[1, 2].map((i) => (
    -

    Gift Cards

    +

    + {t("giftCards")} +

    {cards.length === 0 ? (

    - No gift cards + {t("noGiftCards")}

    -

    - You don't have any gift cards associated with your account yet. -

    +

    {t("noGiftCardsDescription")}

    ) : ( <> @@ -216,7 +226,7 @@ export default function GiftCardsPage() { {activeCards.length > 0 && (

    - Active Gift Cards ({activeCards.length}) + {t("activeGiftCards")} ({activeCards.length})

    {activeCards.map((card) => ( @@ -230,7 +240,7 @@ export default function GiftCardsPage() { {inactiveCards.length > 0 && (

    - Expired / Redeemed ({inactiveCards.length}) + {t("expiredRedeemed")} ({inactiveCards.length})

    {inactiveCards.map((card) => ( diff --git a/src/app/[country]/[locale]/(storefront)/account/layout.tsx b/src/app/[country]/[locale]/(storefront)/account/layout.tsx index e213ff9a..249c93b6 100644 --- a/src/app/[country]/[locale]/(storefront)/account/layout.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/layout.tsx @@ -12,27 +12,30 @@ import { } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect } from "react"; import { Button } from "@/components/ui/button"; import { useAuth } from "@/contexts/AuthContext"; import { extractBasePath } from "@/lib/utils/path"; -const navItems: { +function useNavItems(t: ReturnType>): { href: string; label: string; icon: LucideIcon; -}[] = [ - { href: "/account", label: "Overview", icon: Home }, - { href: "/account/orders", label: "Orders", icon: ShoppingBag }, - { href: "/account/addresses", label: "Addresses", icon: MapPin }, - { - href: "/account/credit-cards", - label: "Payment Methods", - icon: CreditCard, - }, - { href: "/account/gift-cards", label: "Gift Cards", icon: Gift }, - { href: "/account/profile", label: "Profile", icon: User }, -]; +}[] { + return [ + { href: "/account", label: t("overview"), icon: Home }, + { href: "/account/orders", label: t("orders"), icon: ShoppingBag }, + { href: "/account/addresses", label: t("addresses"), icon: MapPin }, + { + href: "/account/credit-cards", + label: t("paymentMethods"), + icon: CreditCard, + }, + { href: "/account/gift-cards", label: t("giftCards"), icon: Gift }, + { href: "/account/profile", label: t("profile"), icon: User }, + ]; +} function ContentSkeleton() { return ( @@ -66,6 +69,8 @@ function AccountShell({ onLogout, isLoading, }: AccountShellProps) { + const t = useTranslations("account"); + const navItems = useNavItems(t); return (
    @@ -84,7 +89,7 @@ function AccountShell({

    {user?.first_name ? `${user.first_name} ${user.last_name || ""}`.trim() - : "My Account"} + : t("myAccount")}

    {user?.email} @@ -125,7 +130,7 @@ function AccountShell({

    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index dd121543..a20fb91e 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -11,6 +11,7 @@ import type { import { ChevronLeft, CircleAlert } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { use, useEffect, useState } from "react"; import { PaymentIcon } from "react-svg-credit-card-payment-icons"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -47,6 +48,7 @@ function getShipmentStatusColor(state: string): string { } function PaymentSourceInfo({ payment }: { payment: Payment }) { + const t = useTranslations("orders"); const source = payment.source; if (payment.source_type === "credit_card" && source) { @@ -60,10 +62,16 @@ function PaymentSourceInfo({ payment }: { payment: Payment }) { />

    - {getCardLabel(card.cc_type)} ending in {card.last_digits} + {t("cardEndingIn", { + label: getCardLabel(card.cc_type), + digits: card.last_digits, + })}

    - Expires {String(card.month).padStart(2, "0")}/{card.year} + {t("cardExpires", { + month: String(card.month).padStart(2, "0"), + year: card.year, + })}

    @@ -74,7 +82,7 @@ function PaymentSourceInfo({ payment }: { payment: Payment }) { const credit = source as StoreCredit; return (
    -

    Store Credit

    +

    {t("storeCredit")}

    Applied {payment.display_amount} — {credit.display_amount_remaining}{" "} remaining @@ -119,6 +127,7 @@ function LineItemCard({ item: Order["items"][number]; basePath: string; }) { + const t = useTranslations("orders"); return (

    {/* Image */} @@ -147,7 +156,9 @@ function LineItemCard({ {item.options_text && (

    {item.options_text}

    )} -

    Qty: {item.quantity}

    +

    + {t("qty", { quantity: item.quantity })} +

    {/* Shipment header */} @@ -183,7 +195,7 @@ function ShipmentBlock({ {shipAddress && (

    - Delivery Address + {t("deliveryAddress")}

    @@ -191,7 +203,7 @@ function ShipmentBlock({

    - Shipping Method + {t("shippingMethod")}

    {shipment.shipping_method?.name || "Canceled"} @@ -215,12 +227,12 @@ function ShipmentBlock({ target="_blank" rel="noopener noreferrer" > - Track Items + {t("trackItems")} ) : ( )}

    @@ -265,6 +277,8 @@ interface OrderDetailPageProps { } export default function OrderDetailPage({ params }: OrderDetailPageProps) { + const t = useTranslations("orders"); + const tc = useTranslations("common"); const { id } = use(params); const pathname = usePathname(); const basePath = extractBasePath(pathname); @@ -297,7 +311,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { return (

    - Order not found + {t("orderNotFound")}

    The order you're looking for doesn't exist. @@ -306,7 +320,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { href={`${basePath}/account/orders`} className="text-primary hover:text-primary font-medium" > - Back to orders + {t("backToOrders")}

    ); @@ -322,7 +336,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { className="text-sm text-gray-500 hover:text-gray-700 mb-4 inline-flex items-center gap-1" > - Back to orders + {t("backToOrders")} {/* Title */} @@ -330,7 +344,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { Order #{order.number}

    - Placed on {formatDate(order.completed_at)} + {t("placedOn", { date: formatDate(order.completed_at) })}

    {/* Shipments with line items */} @@ -360,7 +374,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { {order.special_instructions && (

    - Special Instructions + {t("specialInstructions")}

    {order.special_instructions}

    @@ -372,7 +386,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { {order.bill_address && (

    - Billing Address + {t("billingAddress")}

    @@ -380,7 +394,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { {order.payments && order.payments.length > 0 && (

    - Payment Information + {t("paymentInformation")}

    {order.payments .filter((p) => p.state !== "void" && p.state !== "invalid") @@ -401,16 +415,16 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) {
    - Subtotal + {tc("subtotal")} {order.display_item_total}
    - Shipping + {tc("shipping")} {order.display_ship_total}
    {order.promo_total && Number.parseFloat(order.promo_total) !== 0 && (
    - Discount + {tc("discount")} {order.display_promo_total} @@ -418,12 +432,12 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { )} {Number.parseFloat(order.tax_total) > 0 && (
    - Tax + {tc("tax")} {order.display_tax_total}
    )}
    - Total + {tc("total")} {order.display_total}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx index 1593614e..bd411258 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -4,20 +4,40 @@ import type { Order } from "@spree/sdk"; import { ShoppingBag } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { getOrders } from "@/lib/data/orders"; import { extractBasePath } from "@/lib/utils/path"; -function formatDate(dateString: string | null): string { +function formatDate(dateString: string | null, locale: string): string { if (!dateString) return "-"; - return new Date(dateString).toLocaleDateString("en-US", { + return new Date(dateString).toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric", }); } +const PAYMENT_STATE_KEY: Record = { + paid: "paid", + balance_due: "balanceDue", + credit_owed: "creditOwed", + pending: "pending", + failed: "failed", + void: "void", +}; + +const SHIPMENT_STATE_KEY: Record = { + shipped: "shipped", + partial: "partiallyShipped", + delivered: "delivered", + ready: "ready", + pending: "pending", + canceled: "canceled", + backorder: "backorder", +}; + function getPaymentStatusColor(state: string | null): string { switch (state) { case "paid": @@ -49,6 +69,9 @@ function getShipmentStatusColor(state: string | null): string { } export default function OrdersPage() { + const t = useTranslations("orders"); + const tc = useTranslations("common"); + const locale = useLocale(); const pathname = usePathname(); const basePath = extractBasePath(pathname); const [orders, setOrders] = useState([]); @@ -67,7 +90,9 @@ export default function OrdersPage() { if (loading) { return (
    -

    Order History

    +

    + {t("orderHistory")} +

    {[1, 2, 3].map((i) => (
    -

    Order History

    +

    + {t("orderHistory")} +

    {orders.length === 0 ? (

    - No orders yet + {t("noOrders")}

    -

    - When you place orders, they will appear here. -

    +

    {t("noOrdersDescription")}

    ) : ( @@ -107,22 +132,22 @@ export default function OrdersPage() { - Order + {t("order")} - Date + {t("date")} - Payment + {t("payment")} - Shipment + {t("shipment")} - Total + {tc("total")} - Actions + {t("actions")} @@ -136,21 +161,31 @@ export default function OrdersPage() { - {formatDate(order.completed_at)} + {formatDate(order.completed_at, locale)} - {order.payment_state?.replace("_", " ") || "N/A"} + {order.payment_state + ? t( + PAYMENT_STATE_KEY[order.payment_state] || + order.payment_state, + ) + : t("notAvailable")} - {order.shipment_state || "N/A"} + {order.shipment_state + ? t( + SHIPMENT_STATE_KEY[order.shipment_state] || + order.shipment_state, + ) + : t("notAvailable")} @@ -161,7 +196,7 @@ export default function OrdersPage() { diff --git a/src/app/[country]/[locale]/(storefront)/account/page.tsx b/src/app/[country]/[locale]/(storefront)/account/page.tsx index 63530c7c..8dd4890a 100644 --- a/src/app/[country]/[locale]/(storefront)/account/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/page.tsx @@ -11,6 +11,7 @@ import { } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -20,6 +21,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { extractBasePath } from "@/lib/utils/path"; export default function AccountPage() { + const t = useTranslations("account"); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -47,7 +49,7 @@ export default function AccountPage() { router.push(redirectUrl); } } else { - setError(result.error || "Invalid email or password"); + setError(result.error || t("invalidCredentials")); } setLoading(false); }; @@ -70,10 +72,8 @@ export default function AccountPage() { return (
    -

    My Account

    -

    - Sign in to access your account and order history. -

    +

    {t("myAccount")}

    +

    {t("signInDescription")}

    @@ -86,7 +86,7 @@ export default function AccountPage() { )} - Email + {t("email")} - Password + {t("password")}
    setShowPassword(!showPassword)} aria-label={ - showPassword ? "Hide password" : "Show password" + showPassword ? t("hidePassword") : t("showPassword") } > {showPassword ? ( @@ -140,18 +140,18 @@ export default function AccountPage() { size="lg" className="w-full" > - {loading ? "Signing in..." : "Sign In"} + {loading ? t("signingIn") : t("signIn")}
    - Don't have an account? + {t("dontHaveAccount")} - Sign up + {t("signUp")}
    @@ -163,7 +163,7 @@ export default function AccountPage() { return (

    - Account Overview + {t("accountOverview")}

    @@ -177,10 +177,10 @@ export default function AccountPage() {

    - Order History + {t("orderHistory")}

    - View your past orders and track shipments + {t("orderHistoryDescription")}

    @@ -195,9 +195,11 @@ export default function AccountPage() {
    -

    Addresses

    +

    + {t("addresses")} +

    - Manage your shipping and billing addresses + {t("addressesDescription")}

    @@ -213,10 +215,10 @@ export default function AccountPage() {

    - Payment Methods + {t("paymentMethods")}

    - Manage your saved credit cards + {t("paymentMethodsDescription")}

    @@ -231,9 +233,11 @@ export default function AccountPage() {
    -

    Profile

    +

    + {t("profile")} +

    - Update your personal information + {t("profileDescription")}

    diff --git a/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx b/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx index 044d84c0..78930ffc 100644 --- a/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx @@ -1,6 +1,7 @@ "use client"; import { CircleAlert } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { toast } from "sonner"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -23,6 +24,7 @@ function ProfileForm({ }; refreshUser: () => Promise; }) { + const t = useTranslations("profile"); // Initialize form data from user props - no useEffect needed const [formData, setFormData] = useState({ first_name: user.first_name || "", @@ -40,10 +42,10 @@ function ProfileForm({ const result = await updateCustomer(formData); if (result.success) { - toast.success("Profile updated successfully!"); + toast.success(t("profileUpdated")); await refreshUser(); } else { - setError(result.error || "Failed to update profile"); + setError(result.error || t("failedToUpdate")); } setSaving(false); @@ -51,7 +53,7 @@ function ProfileForm({ return (
    -

    Profile

    +

    {t("profile")}

    @@ -64,7 +66,7 @@ function ProfileForm({ )}
    - First Name + {t("firstName")} - Last Name + {t("lastName")} - Email Address + {t("emailAddress")}
    @@ -114,13 +116,15 @@ function ProfileForm({

    - Account Information + {t("accountInformation")}

    -
    Account ID
    +
    + {t("accountId")} +
    {user.id}
    @@ -136,12 +140,13 @@ function ProfileForm({ // Main page component - uses key prop to reset form when user changes export default function ProfilePage() { + const t = useTranslations("profile"); const { user, refreshUser } = useAuth(); if (!user) { return (
    -

    Loading profile...

    +

    {t("loadingProfile")}

    ); } diff --git a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx index 99547a4a..4be9a094 100644 --- a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx @@ -3,6 +3,7 @@ import { CircleAlert, Eye, EyeOff } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -12,6 +13,8 @@ import { useAuth } from "@/contexts/AuthContext"; import { extractBasePath } from "@/lib/utils/path"; export default function RegisterPage() { + const t = useTranslations("register"); + const ta = useTranslations("account"); const router = useRouter(); const pathname = usePathname(); const basePath = extractBasePath(pathname); @@ -44,12 +47,12 @@ export default function RegisterPage() { setError(null); if (password !== passwordConfirmation) { - setError("Passwords do not match"); + setError(t("passwordsDontMatch")); return; } if (password.length < 6) { - setError("Password must be at least 6 characters"); + setError(t("passwordTooShort")); return; } @@ -66,7 +69,7 @@ export default function RegisterPage() { if (result.success) { router.push(`${basePath}/account`); } else { - setError(result.error || "Registration failed. Please try again."); + setError(result.error || t("registrationFailed")); } } catch { setError("An unexpected error occurred. Please try again."); @@ -78,8 +81,10 @@ export default function RegisterPage() { return (
    -

    Create Account

    -

    Sign up to start shopping with us.

    +

    + {t("createAccount")} +

    +

    {t("signUpDescription")}

    @@ -118,7 +123,7 @@ export default function RegisterPage() {
    - Email + {ta("email")} - Password + {ta("password")}
    setShowPassword(!showPassword)} - aria-label={showPassword ? "Hide password" : "Show password"} + aria-label={ + showPassword ? ta("hidePassword") : ta("showPassword") + } > {showPassword ? ( @@ -162,7 +169,7 @@ export default function RegisterPage() { - Confirm Password + {t("confirmPassword")}
    {showPasswordConfirmation ? ( @@ -204,18 +213,18 @@ export default function RegisterPage() { size="lg" className="w-full" > - {submitting ? "Creating account..." : "Create Account"} + {submitting ? t("creatingAccount") : t("createAccount")}
    - Already have an account? + {t("alreadyHaveAccount")} - Sign in + {t("signIn")}
    diff --git a/src/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsx b/src/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsx index 55471133..2edebe0a 100644 --- a/src/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsx +++ b/src/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsx @@ -1,6 +1,7 @@ "use client"; import type { ProductListParams } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { ProductListingLayout } from "@/components/products/ProductListingLayout"; import { useStore } from "@/contexts/StoreContext"; @@ -21,6 +22,7 @@ export function CategoryProductsContent({ categoryName, basePath, }: CategoryProductsContentProps) { + const t = useTranslations("products"); const { currency } = useStore(); const fetchFn = useCallback( @@ -67,7 +69,7 @@ export function CategoryProductsContent({ onFilterChange={listing.handleFilterChange} listId={listId} listName={listName} - emptyMessage="No products found matching your filters." + emptyMessage={t("noProductsMatchingFilters")} /> ); } diff --git a/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx b/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx index 40f84be7..83f429ef 100644 --- a/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; +import { getTranslations } from "next-intl/server"; import { Breadcrumbs } from "@/components/navigation/Breadcrumbs"; import { CategoryBanner } from "@/components/navigation/CategoryBanner"; import { JsonLd } from "@/components/seo/JsonLd"; @@ -30,6 +31,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) { const { country, locale, permalink } = await params; const fullPermalink = permalink.join("/"); const basePath = `/${country}/${locale}`; + const t = await getTranslations("products"); let category; try { @@ -72,7 +74,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) { {category.children && category.children.length > 0 && (

    - Subcategories + {t("subcategoriesHeading")}

    {category.children.map((child) => ( diff --git a/src/app/[country]/[locale]/(storefront)/cart/page.tsx b/src/app/[country]/[locale]/(storefront)/cart/page.tsx index 11fc8021..fcb7822d 100644 --- a/src/app/[country]/[locale]/(storefront)/cart/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/cart/page.tsx @@ -4,6 +4,7 @@ import type { LineItem } from "@spree/sdk"; import { ShoppingBag } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; @@ -17,6 +18,8 @@ export default function CartPage() { const pathname = usePathname(); const basePath = extractBasePath(pathname); const viewCartFiredRef = useRef(false); + const t = useTranslations("cart"); + const tc = useTranslations("common"); // Track view_cart when cart loads with items useEffect(() => { @@ -57,14 +60,14 @@ export default function CartPage() { strokeWidth={1} />

    - Your cart is empty + {t("emptyCart")}

    -

    - Looks like you haven't added anything to your cart yet. -

    +

    {t("emptyCartDescription")}

    @@ -74,7 +77,9 @@ export default function CartPage() { return (
    -

    Shopping Cart

    +

    + {t("shoppingCart")} +

    {/* Cart Items */} @@ -120,10 +125,10 @@ export default function CartPage() {
    @@ -134,33 +139,37 @@ export default function CartPage() { {/* Order Summary */}
    -

    Order Summary

    +

    + {tc("orderSummary")} +

    -
    Subtotal
    +
    {tc("subtotal")}
    {cart.display_item_total}
    {cart.promo_total && parseFloat(cart.promo_total) < 0 && (
    -
    Discount
    +
    {tc("discount")}
    {cart.display_promo_total}
    )} {cart.ship_total && parseFloat(cart.ship_total) > 0 && (
    -
    Shipping
    +
    {tc("shipping")}
    {cart.display_ship_total}
    )} {cart.tax_total && parseFloat(cart.tax_total) > 0 && (
    -
    Tax
    +
    {tc("tax")}
    {cart.display_tax_total}
    )}
    -
    Total
    +
    + {tc("total")} +
    {cart.display_total}
    @@ -170,11 +179,13 @@ export default function CartPage() {
    diff --git a/src/app/[country]/[locale]/(storefront)/page.tsx b/src/app/[country]/[locale]/(storefront)/page.tsx index 5de9a508..fa018aca 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -1,9 +1,9 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { getTranslations } from "next-intl/server"; import { ProductCarousel } from "@/components/products/ProductCarousel"; import { Button } from "@/components/ui/button"; import { generateHomeMetadata } from "@/lib/metadata/home"; -import { getStoreDescription, getStoreName } from "@/lib/seo"; interface HomePageProps { params: Promise<{ @@ -22,6 +22,7 @@ export async function generateMetadata({ export default async function HomePage({ params }: HomePageProps) { const { country, locale } = await params; const basePath = `/${country}/${locale}`; + const t = await getTranslations("home"); return (
    @@ -30,17 +31,19 @@ export default async function HomePage({ params }: HomePageProps) {

    - Welcome to {getStoreName()} + {t("welcome")}

    - {getStoreDescription()} + {t("heroDescription")}

    @@ -51,10 +54,10 @@ export default async function HomePage({ params }: HomePageProps) {

    - Featured Products + {t("featuredProducts")}

    diff --git a/src/app/[country]/[locale]/(storefront)/products/ProductsContent.tsx b/src/app/[country]/[locale]/(storefront)/products/ProductsContent.tsx index a2f79be9..2bb80348 100644 --- a/src/app/[country]/[locale]/(storefront)/products/ProductsContent.tsx +++ b/src/app/[country]/[locale]/(storefront)/products/ProductsContent.tsx @@ -2,6 +2,7 @@ import type { ProductListParams } from "@spree/sdk"; import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { ProductListingLayout } from "@/components/products/ProductListingLayout"; import { useStore } from "@/contexts/StoreContext"; @@ -16,6 +17,7 @@ interface ProductsContentProps { export function ProductsContent({ basePath }: ProductsContentProps) { const searchParams = useSearchParams(); const { currency } = useStore(); + const t = useTranslations("products"); const query = searchParams.get("q") || ""; const fetchFn = useCallback( @@ -68,17 +70,18 @@ export function ProductsContent({ basePath }: ProductsContentProps) { {query ? ( <>

    - Search results for “{query}” + {t("searchResultsFor", { query })}

    - {listing.totalCount}{" "} - {listing.totalCount === 1 ? "product" : "products"} found + {t("productsFound", { count: listing.totalCount })}

    ) : ( <> -

    All Products

    -

    Browse our complete collection

    +

    + {t("allProducts")} +

    +

    {t("browseCollection")}

    )}
    @@ -90,9 +93,7 @@ export function ProductsContent({ basePath }: ProductsContentProps) { listId={listId} listName={listName} emptyMessage={ - query - ? `We couldn't find any products matching "${query}"` - : "Try adjusting your filters" + query ? t("noMatchingProducts", { query }) : t("tryAdjustingFilters") } />
    diff --git a/src/app/[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx b/src/app/[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx index 0ce3b677..823f2a73 100644 --- a/src/app/[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx +++ b/src/app/[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx @@ -2,6 +2,7 @@ import type { Product, Image as SpreeImage, Variant } from "@spree/sdk"; import { CircleCheckBig, CircleX, Loader2, ShoppingBag } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useMemo, useState } from "react"; import { MediaGallery } from "@/components/products/MediaGallery"; import { VariantPicker } from "@/components/products/VariantPicker"; @@ -19,6 +20,7 @@ interface ProductDetailsProps { export function ProductDetails({ product, basePath }: ProductDetailsProps) { const { addItem } = useCart(); const { currency } = useStore(); + const t = useTranslations("products"); // Filter out master variant from variants list const variants = useMemo(() => { @@ -135,7 +137,7 @@ export function ProductDetails({ product, basePath }: ProductDetailsProps) { {strikethroughPrice} - Sale + {t("sale")} )} @@ -146,12 +148,12 @@ export function ProductDetails({ product, basePath }: ProductDetailsProps) { {inStock ? ( - In Stock + {t("inStock")} ) : ( - Out of Stock + {t("outOfStock")} )}
    @@ -187,15 +189,15 @@ export function ProductDetails({ product, basePath }: ProductDetailsProps) { {loading ? ( <> - Adding... + {t("adding")} ) : isPurchasable ? ( <> - Add to Cart + {t("addToCart")} ) : ( - "Out of Stock" + t("outOfStock") )}
    @@ -205,7 +207,7 @@ export function ProductDetails({ product, basePath }: ProductDetailsProps) { {product.description && (

    - Description + {t("description")}

    -

    Details

    +

    + {t("details")} +

    {selectedVariant?.sku && (
    -
    SKU
    +
    {t("sku")}
    {selectedVariant.sku}
    @@ -228,7 +232,7 @@ export function ProductDetails({ product, basePath }: ProductDetailsProps) { )} {selectedVariant?.options_text && (
    -
    Options
    +
    {t("options")}
    {selectedVariant.options_text}
    diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 746352b5..693d6f28 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,14 @@ import { GoogleTagManager } from "@next/third-parties/google"; import type { Metadata } from "next"; import { Geist } from "next/font/google"; +import { cookies } from "next/headers"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; import "./globals.css"; import { CartDrawer } from "@/components/cart/CartDrawer"; import { Toaster } from "@/components/ui/sonner"; import { CartProvider } from "@/contexts/CartContext"; +import { getStoreDescription, getStoreName } from "@/lib/seo"; const gtmId = process.env.GTM_ID; @@ -13,34 +17,38 @@ const geist = Geist({ subsets: ["latin"], }); -const rootStoreName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; +const rootStoreName = getStoreName(); export const metadata: Metadata = { title: { template: `%s | ${rootStoreName}`, default: rootStoreName, }, - description: - process.env.NEXT_PUBLIC_STORE_DESCRIPTION || - "A modern e-commerce storefront powered by Spree Commerce and Next.js.", + description: getStoreDescription(), }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const cookieStore = await cookies(); + const locale = cookieStore.get("spree_locale")?.value || "en"; + const messages = await getMessages(); + return ( - + {gtmId && } - - {children} - - - + + + {children} + + + + ); diff --git a/src/components/cart/CartDrawer.tsx b/src/components/cart/CartDrawer.tsx index e5756dda..8d858ec4 100644 --- a/src/components/cart/CartDrawer.tsx +++ b/src/components/cart/CartDrawer.tsx @@ -3,6 +3,7 @@ import { ShoppingBag, Trash, X } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; @@ -29,6 +30,8 @@ export function CartDrawer() { removeItem, itemCount, } = useCart(); + const t = useTranslations("cart"); + const tc = useTranslations("common"); const pathname = usePathname(); const basePath = extractBasePath(pathname); const viewCartFiredRef = useRef(false); @@ -72,16 +75,18 @@ export function CartDrawer() { - Cart + {t("cart")} {itemCount > 0 && ( - ({itemCount} items) + + ({t("itemCount", { count: itemCount })}) + )} @@ -105,13 +110,14 @@ export function CartDrawer() { className="w-16 h-16 text-gray-300 mb-4" strokeWidth={1} /> -

    Your cart is empty

    +

    {t("emptyCart")}

    +

    {t("emptyCartDescription")}

    - Continue Shopping + {tc("continueShopping")}
    ) : ( @@ -154,7 +160,7 @@ export function CartDrawer() { } }} disabled={updating} - aria-label="Remove item" + aria-label={t("removeItem")} > @@ -213,18 +219,20 @@ export function CartDrawer() { {/* Summary */}
    - Subtotal + {tc("subtotal")} {cart?.display_item_total}
    {cart?.promo_total && parseFloat(cart.promo_total) < 0 && (
    - Discount + {tc("discount")} {cart.display_promo_total}
    )}
    - Shipping - Calculated on checkout + {tc("shipping")} + + {t("shippingCalculatedAtCheckout")} +
    @@ -235,12 +243,12 @@ export function CartDrawer() { href={`${basePath}/checkout/${cart?.id}`} onClick={closeCart} > - Checkout + {t("checkout")}
    diff --git a/src/components/checkout/AddressEditModal.tsx b/src/components/checkout/AddressEditModal.tsx index 7e9472eb..0d09b13b 100644 --- a/src/components/checkout/AddressEditModal.tsx +++ b/src/components/checkout/AddressEditModal.tsx @@ -2,6 +2,7 @@ import type { Country, State } from "@spree/sdk"; import { CircleAlert } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -47,6 +48,8 @@ export function AddressEditModal({ onClose, title, }: AddressEditModalProps) { + const t = useTranslations("address"); + const tc = useTranslations("common"); const [formData, setFormData] = useState( address ? addressToFormData(address) : { ...emptyAddress }, ); @@ -101,14 +104,14 @@ export function AddressEditModal({ await onSave(formDataToAddress(formData), address?.id); onClose(); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to save address"); + setError(err instanceof Error ? err.message : t("failedToSave")); } finally { setSaving(false); } }; const modalTitle = - title ?? (address?.id ? "Edit Address" : "Add New Address"); + title ?? (address?.id ? t("editAddress") : t("addNewAddress")); return (
    diff --git a/src/components/checkout/AddressFormFields.tsx b/src/components/checkout/AddressFormFields.tsx index a59fdf1d..a6debbf1 100644 --- a/src/components/checkout/AddressFormFields.tsx +++ b/src/components/checkout/AddressFormFields.tsx @@ -1,6 +1,7 @@ "use client"; import type { Country, State } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { Field, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { @@ -26,12 +27,16 @@ export function AddressFormFields({ onChange, idPrefix, }: AddressFormFieldsProps) { + const t = useTranslations("address"); + const tc = useTranslations("common"); const hasStates = states.length > 0; return (
    - First name + + {t("firstName")} + - Last name + + {t("lastName")} + - - Company (optional) - + {t("company")} - Address + + {t("streetAddress")} + onChange("address1", e.target.value)} - placeholder="Street address" + placeholder={t("streetAddress")} /> - Apartment, suite, etc. (optional) + {t("apartment")} - City + {t("city")} - Country + {t("country")} - Select a country + {t("selectCountry")} {countries.map((country) => ( @@ -120,10 +127,12 @@ export function AddressFormFields({ - State / Province + + {t("stateProvince")} + {loadingStates ? ( - Loading... + {tc("loading")} ) : hasStates ? ( - Select a state + {t("selectState")} {states.map((state) => ( @@ -148,15 +157,13 @@ export function AddressFormFields({ id={`${idPrefix}-state`} value={address.state_name} onChange={(e) => onChange("state_name", e.target.value)} - placeholder="State or province" + placeholder={t("stateOrProvince")} /> )} - - ZIP / Postal code - + {t("zipCode")} - Phone (optional) + {t("phone")} { if (savedAddresses.length === 0) return "new"; @@ -78,7 +81,9 @@ export function AddressSelector({ {/* Saved addresses selection */} {savedAddresses.length > 0 && (
    -

    Select an address

    +

    + {t("selectAnAddress")} +

    {savedAddresses.map((address) => (
    - Edit + {tc("edit")} )}
    @@ -150,7 +155,7 @@ export function AddressSelector({ className="h-4 w-4 text-primary border-gray-300 focus:[outline:1px_solid_black]" /> - Use a different address + {t("useDifferentAddress")}
    diff --git a/src/components/checkout/AddressStep.tsx b/src/components/checkout/AddressStep.tsx index 89733285..572322e4 100644 --- a/src/components/checkout/AddressStep.tsx +++ b/src/components/checkout/AddressStep.tsx @@ -10,6 +10,7 @@ import type { } from "@spree/sdk"; import { InfoIcon } from "lucide-react"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import { useEffect, useState, useTransition } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -54,6 +55,9 @@ export function AddressStep({ onUpdateSavedAddress, processing, }: AddressStepProps) { + const t = useTranslations("checkout"); + const tc = useTranslations("common"); + const ta = useTranslations("address"); const [email, setEmail] = useState(order.email || ""); const [shipAddress, setShipAddress] = useState(() => addressToFormData(order.ship_address), @@ -134,10 +138,10 @@ export function AddressStep({ - Already have an account? + {t("signInPrompt")}
    - Sign in to access your saved - addresses and order history. + {t("signIn")}{" "} + {t("signInDescription")}
    )} @@ -145,10 +149,10 @@ export function AddressStep({ {/* Contact Information */}

    - Contact Information + {t("contactInformation")}

    - Email address + {t("emailAddress")} setEmail(e.target.value)} disabled={isAuthenticated} - placeholder="you@example.com" + placeholder={t("emailPlaceholder")} /> {isAuthenticated && ( - - Using your account email address - + {t("usingAccountEmail")} )}
    @@ -169,7 +171,7 @@ export function AddressStep({ {/* Shipping Address */}

    - Shipping Address + {t("shippingAddress")}

    {isAuthenticated && savedAddresses.length > 0 ? (
    @@ -215,7 +217,7 @@ export function AddressStep({ fetchStates={fetchStates} onSave={handleSaveEditedAddress} onClose={() => setEditingAddress(null)} - title="Edit Address" + title={ta("editAddress")} /> )} diff --git a/src/components/checkout/CouponCode.tsx b/src/components/checkout/CouponCode.tsx index f6506f49..4c4dde41 100644 --- a/src/components/checkout/CouponCode.tsx +++ b/src/components/checkout/CouponCode.tsx @@ -2,6 +2,7 @@ import type { Cart, Order } from "@spree/sdk"; import { CheckCircle } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -17,6 +18,7 @@ interface CouponCodeProps { } export function CouponCode({ order, onApply, onRemove }: CouponCodeProps) { + const t = useTranslations("coupon"); const [code, setCode] = useState(""); const [applying, setApplying] = useState(false); const [removing, setRemoving] = useState(null); @@ -37,7 +39,7 @@ export function CouponCode({ order, onApply, onRemove }: CouponCodeProps) { if (result.success) { setCode(""); } else { - setError(result.error || "Invalid coupon code"); + setError(result.error || t("invalidCode")); } setApplying(false); @@ -49,7 +51,7 @@ export function CouponCode({ order, onApply, onRemove }: CouponCodeProps) { const result = await onRemove(promotionId); if (!result.success) { - setError(result.error || "Failed to remove coupon code"); + setError(result.error || t("failedToRemove")); } setRemoving(null); @@ -78,10 +80,13 @@ export function CouponCode({ order, onApply, onRemove }: CouponCodeProps) { variant="destructive" size="sm" className="absolute top-2 right-2" + aria-label={t("removeCoupon", { + code: promotion.code || promotion.name, + })} onClick={() => handleRemove(promotion.id)} disabled={removing === promotion.id} > - {removing === promotion.id ? "..." : "Remove"} + {removing === promotion.id ? t("removing") : t("remove")} ))} @@ -101,13 +106,13 @@ export function CouponCode({ order, onApply, onRemove }: CouponCodeProps) { setCode(e.target.value); setError(null); }} - placeholder="Gift card or discount code" - aria-label="Gift card or discount code" + placeholder={t("placeholder")} + aria-label={t("placeholder")} aria-invalid={!!error} />
    {error && {error}} diff --git a/src/components/checkout/DeliveryStep.tsx b/src/components/checkout/DeliveryStep.tsx index 338a625b..b1f16eaf 100644 --- a/src/components/checkout/DeliveryStep.tsx +++ b/src/components/checkout/DeliveryStep.tsx @@ -1,6 +1,7 @@ "use client"; import type { Cart, Order, Shipment } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; interface DeliveryStepProps { @@ -20,6 +21,8 @@ export function DeliveryStep({ onBack, processing, }: DeliveryStepProps) { + const t = useTranslations("checkout"); + const tc = useTranslations("common"); // Check if all shipments have a selected rate const allRatesSelected = shipments.every((shipment) => shipment.shipping_rates.some((rate) => rate.selected), @@ -36,11 +39,11 @@ export function DeliveryStep({

    - Shipping Address + {t("shippingAddress")}

    {onBack && ( )}
    @@ -60,7 +63,9 @@ export function DeliveryStep({

    {order.ship_address.country_name}

    {order.ship_address.phone && ( -

    Phone: {order.ship_address.phone}

    +

    + {t("phone", { phone: order.ship_address.phone })} +

    )}
    @@ -69,22 +74,28 @@ export function DeliveryStep({ {/* Shipping Methods */}

    - Shipping Method + {t("shippingMethod")}

    {shipments.length === 0 ? ( -

    Loading shipping options...

    +

    {t("loadingShippingOptions")}

    ) : (
    {shipments.map((shipment, index) => (
    {shipments.length > 1 && (

    - Shipment {index + 1} of {shipments.length} + {t("shipmentOf", { + current: index + 1, + total: shipments.length, + })} {shipment.stock_location?.name && ( {" "} - — Ships from {shipment.stock_location.name} + —{" "} + {t("shipsFrom", { + location: shipment.stock_location.name, + })} )}

    @@ -137,7 +148,7 @@ export function DeliveryStep({ onClick={onBack} disabled={processing} > - Back + {tc("back")} )}
    diff --git a/src/components/checkout/OrderSummary.tsx b/src/components/checkout/OrderSummary.tsx index 713202fc..a7e32c03 100644 --- a/src/components/checkout/OrderSummary.tsx +++ b/src/components/checkout/OrderSummary.tsx @@ -1,6 +1,7 @@ "use client"; import type { Cart, Order } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { ProductImage } from "@/components/ui/product-image"; interface OrderSummaryProps { @@ -8,12 +9,13 @@ interface OrderSummaryProps { } export function OrderSummary({ order }: OrderSummaryProps) { + const tc = useTranslations("common"); const lineItems = order.items || []; return (

    - Order Summary + {tc("orderSummary")}

    {/* Line items */} @@ -56,20 +58,20 @@ export function OrderSummary({ order }: OrderSummaryProps) { {/* Totals */}
    - Subtotal + {tc("subtotal")} {order.display_item_total}
    {parseFloat(order.ship_total) > 0 && (
    - Shipping + {tc("shipping")} {order.display_ship_total}
    )} {parseFloat(order.adjustment_total) !== 0 && (
    - Adjustments + {tc("adjustments")} - Discount + {tc("discount")} {order.display_promo_total}
    )} {parseFloat(order.tax_total) > 0 && (
    - Tax + {tc("tax")} {order.display_tax_total}
    )}
    - Total + {tc("total")} {order.display_total}
    diff --git a/src/components/checkout/PaymentStep.tsx b/src/components/checkout/PaymentStep.tsx index 367c2c7c..f3d287a9 100644 --- a/src/components/checkout/PaymentStep.tsx +++ b/src/components/checkout/PaymentStep.tsx @@ -9,6 +9,7 @@ import type { State, } from "@spree/sdk"; import { CircleAlert, CreditCard, Loader2 } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useRef, useState, useTransition } from "react"; import { PaymentIcon } from "react-svg-credit-card-payment-icons"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -56,6 +57,8 @@ export function PaymentStep({ processing, setProcessing, }: PaymentStepProps) { + const t = useTranslations("checkout"); + const tc = useTranslations("common"); // Initialize billing address from order, check if it matches shipping const shipAddressData = addressToFormData(order.ship_address); const billAddressData = addressToFormData(order.bill_address); @@ -121,18 +124,18 @@ export function PaymentStep({ setClientSecret(secret); setPaymentSessionId(result.session.id); } else { - setGatewayError("Failed to initialize payment. Please try again."); + setGatewayError(t("failedToInitPayment")); } } else if (!result.success) { - setGatewayError(result.error || "Failed to create payment session."); + setGatewayError(result.error || t("failedToCreateSession")); } } catch { - setGatewayError("Failed to initialize payment. Please try again."); + setGatewayError(t("failedToInitPayment")); } finally { setLoading(false); } }, - [sessionPaymentMethod, order.id], + [sessionPaymentMethod, order.id, t], ); // On mount: load saved cards (if authenticated), then create initial session — once. @@ -261,7 +264,7 @@ export function PaymentStep({ // 3. Payment succeeded — complete session and order await onPaymentComplete(paymentSessionId); } catch { - setGatewayError("An error occurred during payment. Please try again."); + setGatewayError(t("paymentError")); setProcessing(false); } }; @@ -286,11 +289,11 @@ export function PaymentStep({

    - Shipping Address + {t("shippingAddress")}

    {onBack && ( )}
    @@ -316,7 +319,7 @@ export function PaymentStep({ {/* Billing Address */}

    - Billing Address + {t("billingAddress")}

    @@ -328,7 +331,7 @@ export function PaymentStep({ } /> - Same as shipping address + {t("sameAsShipping")}
    @@ -348,7 +351,7 @@ export function PaymentStep({ {/* Payment Method */}

    - Payment Method + {t("paymentMethod")}

    {/* Saved Cards Selector */} @@ -379,15 +382,16 @@ export function PaymentStep({ />
    - {getCardLabel(card.cc_type)} ending in {card.last_digits} + {getCardLabel(card.cc_type)} {t("endingIn")}{" "} + {card.last_digits} - Exp {String(card.month).padStart(2, "0")}/{card.year} + {t("exp")} {String(card.month).padStart(2, "0")}/{card.year}
    {card.default && ( - Default + {t("default")} )} @@ -410,7 +414,7 @@ export function PaymentStep({ /> - Add new payment method + {t("addNewPaymentMethod")}
    @@ -420,7 +424,7 @@ export function PaymentStep({
    - Loading payment form... + {t("loadingPaymentForm")}
    )} @@ -446,9 +450,7 @@ export function PaymentStep({ className="w-12 h-12 text-gray-400 mx-auto mb-4" strokeWidth={1.5} /> -

    - No payment methods available for this order. -

    +

    {t("noPaymentMethods")}

    )}
    @@ -463,7 +465,7 @@ export function PaymentStep({ onClick={onBack} disabled={processing} > - Back + {tc("back")} )}
    diff --git a/src/components/layout/CountrySwitcher.tsx b/src/components/layout/CountrySwitcher.tsx index d06c6f22..215aebcb 100644 --- a/src/components/layout/CountrySwitcher.tsx +++ b/src/components/layout/CountrySwitcher.tsx @@ -2,6 +2,7 @@ import { Check, ChevronDown } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -30,6 +31,8 @@ function countryToFlag(countryCode: string): string { export function CountrySwitcher() { const { country, currency, countries, setCountry, loading } = useStore(); + const tc = useTranslations("common"); + const currentLocale = useLocale(); const { cart, refreshCart } = useCart(); const router = useRouter(); const pathname = usePathname(); @@ -56,7 +59,13 @@ export function CountrySwitcher() { setStoreCookies(entry.iso.toLowerCase(), newLocale); setCountry(entry.iso.toLowerCase()); - router.push(newPath); + // Hard navigation when locale changes so RootLayout re-renders + // with new messages from NextIntlClientProvider + if (newLocale !== currentLocale) { + window.location.href = newPath; + } else { + router.push(newPath); + } }; if (loading) { @@ -80,7 +89,7 @@ export function CountrySwitcher() { - Select Country + {tc("selectCountry")} {countries.map((c) => { const isSelected = c.iso.toLowerCase() === country.toLowerCase(); return ( diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index a5f4a64b..9fbb3a50 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,10 +1,12 @@ "use client"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import { useStore } from "@/contexts/StoreContext"; export function Footer() { - const { storeName, storeDescription } = useStore(); + const { storeName } = useStore(); + const t = useTranslations("footer"); return (
    @@ -12,19 +14,21 @@ export function Footer() { {/* Brand */}
    {storeName} -

    {storeDescription}

    +

    {t("description")}

    {/* Links */}
    -

    Shop

    +

    + {t("shop")} +

    • - All Products + {t("allProducts")}
    • @@ -32,7 +36,7 @@ export function Footer() { href="/products" className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors" > - Categories + {t("categories")}
    @@ -40,14 +44,16 @@ export function Footer() { {/* Account */}
    -

    Account

    +

    + {t("account")} +

    • - My Account + {t("myAccount")}
    • @@ -55,7 +61,7 @@ export function Footer() { href="/account/orders" className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors" > - Order History + {t("orderHistory")}
    • @@ -63,7 +69,7 @@ export function Footer() { href="/cart" className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors" > - Cart + {t("cart")}
    @@ -71,10 +77,7 @@ export function Footer() {
    -

    - © {new Date().getFullYear()} {storeName}. Powered by Spree - Commerce. -

    +

    {t("copyright", { year: new Date().getFullYear(), storeName })}

    diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 70ae4b93..bbe95fce 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -5,6 +5,7 @@ import { ShoppingBag, User } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { SearchBar } from "@/components/search/SearchBar"; import { Button } from "@/components/ui/button"; import { useCart } from "@/contexts/CartContext"; @@ -21,6 +22,7 @@ export function Header({ rootCategories }: HeaderProps) { const { storeName } = useStore(); const pathname = usePathname(); const basePath = extractBasePath(pathname); + const t = useTranslations("header"); return (
    @@ -72,7 +74,7 @@ export function Header({ rootCategories }: HeaderProps) { variant="ghost" size="icon-lg" onClick={openCart} - aria-label="Open cart" + aria-label={t("openCart")} className="relative" > @@ -85,7 +87,7 @@ export function Header({ rootCategories }: HeaderProps) { {/* Account */} diff --git a/src/components/navigation/Breadcrumbs.tsx b/src/components/navigation/Breadcrumbs.tsx index b2a351bb..6587c176 100644 --- a/src/components/navigation/Breadcrumbs.tsx +++ b/src/components/navigation/Breadcrumbs.tsx @@ -1,6 +1,7 @@ import type { Category } from "@spree/sdk"; import { ChevronRight } from "lucide-react"; import Link from "next/link"; +import { useTranslations } from "next-intl"; interface BreadcrumbsProps { category: Category; @@ -8,8 +9,9 @@ interface BreadcrumbsProps { } export function Breadcrumbs({ category, basePath }: BreadcrumbsProps) { + const t = useTranslations("navigation"); // Build breadcrumb items from ancestors + current category - const items = [{ name: "Home", href: basePath }]; + const items = [{ name: t("home"), href: basePath }]; // Add ancestors (they come from the API in order from root to parent) if (category.ancestors && category.ancestors.length > 0) { @@ -25,7 +27,7 @@ export function Breadcrumbs({ category, basePath }: BreadcrumbsProps) { items.push({ name: category.name, href: "" }); return ( -
    @@ -98,7 +100,7 @@ export const ProductCard = memo(function ProductCard({
    {!product.purchasable && ( - Out of Stock + {t("outOfStock")} )}
    diff --git a/src/components/products/ProductCarousel.tsx b/src/components/products/ProductCarousel.tsx index eb1b6ec4..894f48de 100644 --- a/src/components/products/ProductCarousel.tsx +++ b/src/components/products/ProductCarousel.tsx @@ -8,6 +8,7 @@ import { Swiper as SwiperComponent, SwiperSlide } from "swiper/react"; import "swiper/css"; import "swiper/css/navigation"; import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useTranslations } from "next-intl"; import { ProductCard } from "@/components/products/ProductCard"; import { ProductCardSkeleton } from "@/components/products/ProductCardSkeleton"; import { useCarouselProducts } from "@/hooks/useCarouselProducts"; @@ -26,6 +27,7 @@ export function ProductCarousel({ limit = 8, basePath, }: ProductCarouselProps): ReactElement { + const t = useTranslations("products"); const { products, loading, error } = useCarouselProducts({ categoryId, limit, @@ -79,7 +81,7 @@ export function ProductCarousel({
    diff --git a/src/components/products/VariantPicker.tsx b/src/components/products/VariantPicker.tsx index 7eb563e8..eeb173a7 100644 --- a/src/components/products/VariantPicker.tsx +++ b/src/components/products/VariantPicker.tsx @@ -1,6 +1,7 @@ "use client"; import type { OptionType, Variant } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { useMemo } from "react"; import { Button } from "@/components/ui/button"; import { isColorOption, resolveColor } from "@/lib/utils/color-map"; @@ -18,6 +19,7 @@ export function VariantPicker({ selectedVariant, onVariantChange, }: VariantPickerProps) { + const t = useTranslations("products"); const optionValuesMap = useMemo(() => { const map: Record> = {}; @@ -216,7 +218,7 @@ export function VariantPicker({ {optionValue?.presentation || value} {!isPurchasable && isAvailable && ( - (Out of stock) + {t("outOfStockVariant")} )} diff --git a/src/components/products/__tests__/ProductCard.test.tsx b/src/components/products/__tests__/ProductCard.test.tsx index dc2b2dc8..6cd60b31 100644 --- a/src/components/products/__tests__/ProductCard.test.tsx +++ b/src/components/products/__tests__/ProductCard.test.tsx @@ -3,6 +3,10 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { ProductCard } from "@/components/products/ProductCard"; +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + vi.mock("@/contexts/StoreContext", () => ({ useStore: () => ({ currency: "USD", locale: "en", loading: false }), })); @@ -98,7 +102,7 @@ describe("ProductCard", () => { it("shows Sale badge when on sale", () => { render(); - expect(screen.getByText("Sale")).toBeInTheDocument(); + expect(screen.getByText("sale")).toBeInTheDocument(); }); it("shows strikethrough price when on sale", () => { @@ -113,13 +117,13 @@ describe("ProductCard", () => { it("does not show Sale badge for regular price products", () => { render(); - expect(screen.queryByText("Sale")).not.toBeInTheDocument(); + expect(screen.queryByText("sale")).not.toBeInTheDocument(); }); it("shows Out of Stock for non-purchasable products", () => { render(); - expect(screen.getByText("Out of Stock")).toBeInTheDocument(); + expect(screen.getByText("outOfStock")).toBeInTheDocument(); }); it("renders image when thumbnail_url is provided", () => { diff --git a/src/components/products/filters/AvailabilityDropdownContent.tsx b/src/components/products/filters/AvailabilityDropdownContent.tsx index 3d97c50d..08e39368 100644 --- a/src/components/products/filters/AvailabilityDropdownContent.tsx +++ b/src/components/products/filters/AvailabilityDropdownContent.tsx @@ -1,9 +1,12 @@ +"use client"; + import type { AvailabilityFilter } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { DropdownMenuRadioGroup, DropdownMenuRadioItem, } from "@/components/ui/dropdown-menu"; -import { AVAILABILITY_LABELS } from "@/lib/utils/filters"; +import { getAvailabilityLabel } from "@/lib/utils/filters"; import { type AvailabilityStatus, isAvailabilityStatus } from "@/types/filters"; interface AvailabilityDropdownContentProps { @@ -17,6 +20,8 @@ export function AvailabilityDropdownContent({ selected, onChange, }: AvailabilityDropdownContentProps) { + const t = useTranslations("products"); + return ( - Any availability + + {t("anyAvailability")} + {filter.options.map((option) => ( e.preventDefault()} > - - {AVAILABILITY_LABELS[option.id] || option.id} - + {getAvailabilityLabel(option.id, t)} ({option.count}) diff --git a/src/components/products/filters/FilterChips.tsx b/src/components/products/filters/FilterChips.tsx index fe56ef5a..2cd393d9 100644 --- a/src/components/products/filters/FilterChips.tsx +++ b/src/components/products/filters/FilterChips.tsx @@ -2,8 +2,9 @@ import type { OptionFilter, ProductFiltersResponse } from "@spree/sdk"; import { X } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { AVAILABILITY_LABELS } from "@/lib/utils/filters"; +import { getAvailabilityLabel } from "@/lib/utils/filters"; import { findMatchingBucket, type PriceBucket, @@ -29,6 +30,7 @@ export function FilterChips({ onRemoveAvailability, onClearAll, }: FilterChipsProps) { + const t = useTranslations("products"); const chips: { key: string; label: string; onRemove: () => void }[] = []; if (filtersData) { @@ -63,7 +65,7 @@ export function FilterChips({ ); chips.push({ key: "price", - label: `Price: ${matchingBucket?.label || "Custom"}`, + label: `${t("price")}: ${matchingBucket?.label || t("customPrice")}`, onRemove: onRemovePrice, }); } @@ -71,7 +73,7 @@ export function FilterChips({ if (activeFilters.availability) { chips.push({ key: "availability", - label: `${AVAILABILITY_LABELS[activeFilters.availability] || activeFilters.availability}`, + label: getAvailabilityLabel(activeFilters.availability, t), onRemove: onRemoveAvailability, }); } @@ -89,14 +91,14 @@ export function FilterChips({ ))}
    ); diff --git a/src/components/products/filters/MobileFilterDrawer.tsx b/src/components/products/filters/MobileFilterDrawer.tsx index b6f84140..247b3cc9 100644 --- a/src/components/products/filters/MobileFilterDrawer.tsx +++ b/src/components/products/filters/MobileFilterDrawer.tsx @@ -6,11 +6,15 @@ import type { ProductFiltersResponse, } from "@spree/sdk"; import { Check, X } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { isColorOption, resolveColor } from "@/lib/utils/color-map"; -import { AVAILABILITY_LABELS, getActiveFilterCount } from "@/lib/utils/filters"; +import { + getActiveFilterCount, + getAvailabilityLabel, +} from "@/lib/utils/filters"; import type { PriceBucket } from "@/lib/utils/price-buckets"; import { findMatchingBucket } from "@/lib/utils/price-buckets"; import { @@ -36,6 +40,7 @@ export function MobileFilterDrawer({ priceBuckets, onApply, }: MobileFilterDrawerProps) { + const t = useTranslations("products"); const [stagedFilters, setStagedFilters] = useState(activeFilters); @@ -93,18 +98,18 @@ export function MobileFilterDrawer({ showCloseButton={false} aria-describedby={undefined} > - Filters + {t("filters")}
    -

    Filters

    +

    {t("filters")}

    @@ -147,11 +152,11 @@ export function MobileFilterDrawer({
    {stagedCount > 0 && ( )}
    @@ -247,6 +252,8 @@ function MobilePriceSection({ activeFilters: ActiveFilters; onPriceChange: (min?: number, max?: number) => void; }) { + const t = useTranslations("products"); + if (priceBuckets.length === 0) return null; const selectedBucket = findMatchingBucket( @@ -258,7 +265,7 @@ function MobilePriceSection({ return (

    - Price + {t("price")}

    {priceBuckets.map((bucket) => { @@ -302,10 +309,12 @@ function MobileAvailabilitySection({ selected?: AvailabilityStatus; onChange: (value?: AvailabilityStatus) => void; }) { + const t = useTranslations("products"); + return (

    - Availability + {t("availability")}

    {filter.options.map((option) => { @@ -329,7 +338,7 @@ function MobileAvailabilitySection({ }`} > - {AVAILABILITY_LABELS[option.id] || option.id} + {getAvailabilityLabel(option.id, t)} ({option.count}) {isSelected && ( diff --git a/src/components/products/filters/ProductFilters.tsx b/src/components/products/filters/ProductFilters.tsx index b67edad8..9c648125 100644 --- a/src/components/products/filters/ProductFilters.tsx +++ b/src/components/products/filters/ProductFilters.tsx @@ -7,6 +7,7 @@ import type { ProductFiltersResponse, } from "@spree/sdk"; import { SlidersHorizontal } from "lucide-react"; +import { useLocale, useTranslations } from "next-intl"; import type { JSX } from "react"; import { memo, useCallback, useMemo, useState } from "react"; import { AvailabilityDropdownContent } from "@/components/products/filters/AvailabilityDropdownContent"; @@ -35,6 +36,8 @@ export const FilterBar = memo(function FilterBar({ totalCount, onFilterChange, }: FilterBarProps): JSX.Element | null { + const t = useTranslations("products"); + const locale = useLocale(); const [openDropdownId, setOpenDropdownId] = useState(null); const [showMobileDrawer, setShowMobileDrawer] = useState(false); @@ -98,8 +101,9 @@ export const FilterBar = memo(function FilterBar({ priceFilter.min, priceFilter.max, priceFilter.currency, + { t, locale }, ); - }, [filtersData]); + }, [filtersData, t, locale]); const optionFilters = useMemo(() => { if (!filtersData) return []; @@ -176,7 +180,7 @@ export const FilterBar = memo(function FilterBar({ {hasPriceFilter && ( toggleDropdown("price")} @@ -192,7 +196,7 @@ export const FilterBar = memo(function FilterBar({ {availabilityFilter && ( toggleDropdown("availability")} @@ -209,10 +213,10 @@ export const FilterBar = memo(function FilterBar({
    - {totalCount} {totalCount === 1 ? "product" : "products"} + {t("productCount", { count: totalCount })} toggleDropdown("sort")} onClose={closeDropdown} @@ -238,7 +242,7 @@ export const FilterBar = memo(function FilterBar({ }`} > - Filters + {t("filters")} {hasActiveFilters && ( {totalActiveFilters} @@ -248,7 +252,7 @@ export const FilterBar = memo(function FilterBar({
    toggleDropdown("sort-mobile")} onClose={closeDropdown} diff --git a/src/components/products/filters/SortDropdownContent.tsx b/src/components/products/filters/SortDropdownContent.tsx index 367d1c40..47c56330 100644 --- a/src/components/products/filters/SortDropdownContent.tsx +++ b/src/components/products/filters/SortDropdownContent.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { useTranslations } from "next-intl"; import { DropdownMenuRadioGroup, DropdownMenuRadioItem, @@ -15,11 +18,13 @@ export function SortDropdownContent({ activeSortBy, onSortChange, }: SortDropdownContentProps) { + const t = useTranslations("products"); + return ( {sortOptions.map((option) => ( - {getSortLabel(option.id)} + {getSortLabel(option.id, t)} ))} diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 72a6df1c..823e6449 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -3,6 +3,7 @@ import type { Product } from "@spree/sdk"; import { Search } from "lucide-react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useRef, useState } from "react"; import { InputGroup, @@ -21,6 +22,7 @@ interface SearchBarProps { export function SearchBar({ basePath }: SearchBarProps) { const router = useRouter(); const { currency } = useStore(); + const t = useTranslations("products"); const [query, setQuery] = useState(""); const [suggestions, setSuggestions] = useState([]); const [isOpen, setIsOpen] = useState(false); @@ -157,7 +159,7 @@ export function SearchBar({ basePath }: SearchBarProps) { }} onFocus={() => setIsOpen(true)} onKeyDown={handleKeyDown} - placeholder="Search..." + placeholder={t("search")} role="combobox" aria-expanded={showSuggestions} aria-controls="search-suggestions" @@ -178,7 +180,7 @@ export function SearchBar({ basePath }: SearchBarProps) {
    {loading ? (
    - Searching... + {t("searching")}
    ) : suggestions.length > 0 ? (
      @@ -235,14 +237,14 @@ export function SearchBar({ basePath }: SearchBarProps) { }} className="w-full p-3 text-sm text-primary hover:bg-gray-50 text-center font-medium" > - View all results for “{query}” + {t("viewAllResultsFor", { query })} )}
    ) : query.length >= 2 ? (
    - No products found + {t("noProductsFound")}
    ) : null}
    diff --git a/src/contexts/StoreContext.tsx b/src/contexts/StoreContext.tsx index cd0f1fb9..c1b946e8 100644 --- a/src/contexts/StoreContext.tsx +++ b/src/contexts/StoreContext.tsx @@ -11,6 +11,7 @@ import { useRef, useState, } from "react"; +import { getStoreName } from "@/lib/seo"; import { setStoreCookies } from "@/lib/utils/cookies"; import { getPathWithoutPrefix } from "@/lib/utils/path"; @@ -21,14 +22,10 @@ export interface CountryWithMarket extends Country { marketId: string | null; } -const storeName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; -const storeDescription = - process.env.NEXT_PUBLIC_STORE_DESCRIPTION || - "A modern e-commerce storefront powered by Spree Commerce and Next.js."; +const storeName = getStoreName(); interface StoreContextValue { storeName: string; - storeDescription: string; country: string; locale: string; currency: string; @@ -183,7 +180,6 @@ export function StoreProvider({ const value = useMemo( () => ({ storeName, - storeDescription, country, locale, currency, diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 00000000..00178cb6 --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,30 @@ +import { cookies } from "next/headers"; +import { getRequestConfig } from "next-intl/server"; + +const supportedLocales = ["en", "de", "pl"]; + +export default getRequestConfig(async ({ requestLocale }) => { + // Prefer the route-derived locale (from the [locale] path segment), + // fall back to the spree_locale cookie, then default to "en". + let locale = await requestLocale; + + if (!locale || !supportedLocales.includes(locale)) { + const cookieStore = await cookies(); + locale = cookieStore.get("spree_locale")?.value; + } + + if (!locale || !supportedLocales.includes(locale)) { + locale = "en"; + } + + let messages: IntlMessages; + try { + messages = (await import(`../../messages/${locale}.json`)).default; + } catch { + // Fallback to English if locale file doesn't exist + locale = "en"; + messages = (await import("../../messages/en.json")).default; + } + + return { locale, messages }; +}); diff --git a/src/lib/utils/filters.ts b/src/lib/utils/filters.ts index 33902290..7f265252 100644 --- a/src/lib/utils/filters.ts +++ b/src/lib/utils/filters.ts @@ -21,19 +21,20 @@ export function getActiveFilterCount(filters: ActiveFilters): number { ); } -const SORT_LABELS_CANONICAL: Record = { - manual: "Relevance", - best_selling: "Best Selling", - price: "Price: Low to High", - "-price": "Price: High to Low", - "-available_on": "Newest", - available_on: "Oldest", - name: "Name (A-Z)", - "-name": "Name (Z-A)", +/** Maps sort API keys to translation message keys in the "products" namespace. */ +const SORT_KEY_TO_MESSAGE: Record = { + manual: "manual", + best_selling: "bestSelling", + price: "priceLowHigh", + "-price": "priceHighLow", + "-available_on": "newest", + available_on: "oldest", + name: "nameAZ", + "-name": "nameZA", }; export function normalizeSortKey(key: string): string { - if (key in SORT_LABELS_CANONICAL) return key; + if (key in SORT_KEY_TO_MESSAGE) return key; const match = key.match(/^(\w+)\s+(asc|desc)$/); if (!match) return key; const [, field, direction] = match; @@ -43,11 +44,33 @@ export function normalizeSortKey(key: string): string { return needsNegation ? `-${field}` : field; } -export function getSortLabel(key: string): string { - return SORT_LABELS_CANONICAL[normalizeSortKey(key)] || key; +/** + * Get a translated sort label. + * Pass a `t` function from `useTranslations("products")`. + * Falls back to the raw key if no translator is provided. + */ +export function getSortLabel(key: string, t?: (key: string) => string): string { + const normalized = normalizeSortKey(key); + const messageKey = SORT_KEY_TO_MESSAGE[normalized]; + if (messageKey && t) return t(messageKey); + return key; } -export const AVAILABILITY_LABELS: Record = { - in_stock: "In Stock", - out_of_stock: "Out of Stock", +/** Maps availability API values to translation message keys in the "products" namespace. */ +const AVAILABILITY_KEY_TO_MESSAGE: Record = { + in_stock: "inStock", + out_of_stock: "outOfStock", }; + +/** + * Get a translated availability label. + * Pass a `t` function from `useTranslations("products")`. + */ +export function getAvailabilityLabel( + id: string, + t?: (key: string) => string, +): string { + const messageKey = AVAILABILITY_KEY_TO_MESSAGE[id]; + if (messageKey && t) return t(messageKey); + return id; +} diff --git a/src/lib/utils/price-buckets.ts b/src/lib/utils/price-buckets.ts index 648cae3f..4ddb407f 100644 --- a/src/lib/utils/price-buckets.ts +++ b/src/lib/utils/price-buckets.ts @@ -7,9 +7,13 @@ export interface PriceBucket { const THRESHOLDS = [50, 100, 200]; -function formatCurrency(amount: number, currency: string): string { +function formatCurrency( + amount: number, + currency: string, + locale?: string, +): string { try { - return new Intl.NumberFormat("en", { + return new Intl.NumberFormat(locale || "en", { style: "currency", currency, minimumFractionDigits: 0, @@ -20,26 +24,41 @@ function formatCurrency(amount: number, currency: string): string { } } +/** + * Optional translation helpers for bucket labels. + * Pass `t` from `useTranslations("products")` and a locale string + * to get labels like "Poniżej $50" instead of "Under $50". + */ +interface BucketLabelOptions { + t?: (key: string, values?: Record) => string; + locale?: string; +} + export function generatePriceBuckets( filterMin: number, filterMax: number, currency: string, + options?: BucketLabelOptions, ): PriceBucket[] { + const { t, locale } = options || {}; const buckets: PriceBucket[] = []; if (filterMin < THRESHOLDS[0]) { + const price = formatCurrency(THRESHOLDS[0], currency, locale); buckets.push({ id: `under-${THRESHOLDS[0]}`, - label: `Under ${formatCurrency(THRESHOLDS[0], currency)}`, + label: t ? t("priceUnder", { price }) : `Under ${price}`, max: THRESHOLDS[0], }); } for (let i = 0; i < THRESHOLDS.length - 1; i++) { if (filterMax > THRESHOLDS[i] && filterMin < THRESHOLDS[i + 1]) { + const min = formatCurrency(THRESHOLDS[i], currency, locale); + const max = formatCurrency(THRESHOLDS[i + 1], currency, locale); buckets.push({ id: `${THRESHOLDS[i]}-${THRESHOLDS[i + 1]}`, - label: `${formatCurrency(THRESHOLDS[i], currency)} - ${formatCurrency(THRESHOLDS[i + 1], currency)}`, + label: t ? t("priceRangeBucket", { min, max }) : `${min} - ${max}`, min: THRESHOLDS[i], max: THRESHOLDS[i + 1], }); @@ -48,9 +67,10 @@ export function generatePriceBuckets( const lastThreshold = THRESHOLDS[THRESHOLDS.length - 1]; if (filterMax > lastThreshold) { + const price = formatCurrency(lastThreshold, currency, locale); buckets.push({ id: `${lastThreshold}-plus`, - label: `${formatCurrency(lastThreshold, currency)}+`, + label: t ? t("priceAbove", { price }) : `${price}+`, min: lastThreshold, }); } diff --git a/src/types/next-intl.d.ts b/src/types/next-intl.d.ts new file mode 100644 index 00000000..9490a9ea --- /dev/null +++ b/src/types/next-intl.d.ts @@ -0,0 +1,7 @@ +import type messages from "../../messages/en.json"; + +type Messages = typeof messages; + +declare global { + interface IntlMessages extends Messages {} +} From 5a95ed76cdd8ba3d3958289bb651b06c7c257af9 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Thu, 12 Mar 2026 13:22:05 +0100 Subject: [PATCH 02/28] Fix remaining i18n hardcoded strings and accessibility issues Replace hardcoded English strings with translation keys across checkout, account, and storefront pages. Add fieldset/legend for radio groups in AddressSelector, localize aria-labels, and use getLocale() in root layout for consistent locale resolution. Co-Authored-By: Claude Opus 4.6 --- messages/de.json | 32 +++++--- messages/en.json | 32 +++++--- messages/pl.json | 32 +++++--- .../(checkout)/checkout/[id]/page.tsx | 75 ++++++++++--------- .../[country]/[locale]/(checkout)/layout.tsx | 4 +- .../(checkout)/order-placed/[id]/page.tsx | 6 +- .../(storefront)/account/addresses/page.tsx | 9 ++- .../account/credit-cards/page.tsx | 8 +- .../(storefront)/account/gift-cards/page.tsx | 24 +++--- .../(storefront)/account/orders/[id]/page.tsx | 54 ++++++------- .../(storefront)/account/orders/page.tsx | 55 ++------------ .../(storefront)/account/profile/page.tsx | 4 +- .../(storefront)/account/register/page.tsx | 6 +- .../[locale]/(storefront)/cart/page.tsx | 2 +- .../[country]/[locale]/(storefront)/page.tsx | 6 +- src/app/layout.tsx | 6 +- src/components/checkout/AddressSelector.tsx | 8 +- src/components/navigation/Breadcrumbs.tsx | 2 + src/components/products/ProductCarousel.tsx | 2 +- src/components/search/SearchBar.tsx | 2 +- src/i18n/request.ts | 13 ++-- src/lib/utils/order-status.ts | 53 +++++++++++++ 22 files changed, 251 insertions(+), 184 deletions(-) create mode 100644 src/lib/utils/order-status.ts diff --git a/messages/de.json b/messages/de.json index 4e7e2886..5c8af28f 100644 --- a/messages/de.json +++ b/messages/de.json @@ -37,7 +37,7 @@ "copyright": "© {year} {storeName}. Unterstützt durch Spree Commerce." }, "home": { - "welcome": "Willkommen bei Spree Store", + "welcome": "Willkommen bei {storeName}", "heroDescription": "Entdecken Sie großartige Produkte mit unserem modernen E-Commerce-Erlebnis powered by Spree Commerce.", "shopNow": "Jetzt einkaufen", "browseCategories": "Kategorien durchsuchen", @@ -64,7 +64,8 @@ "shoppingCart": "Warenkorb", "proceedToCheckout": "Zur Kasse gehen", "itemCount": "{count, plural, one {# Artikel} other {# Artikel}}", - "shippingCalculatedAtCheckout": "Wird an der Kasse berechnet" + "shippingCalculatedAtCheckout": "Wird an der Kasse berechnet", + "removeItemLabel": "{name} entfernen" }, "products": { "allProducts": "Alle Produkte", @@ -189,7 +190,8 @@ "failedToCompleteOrder": "Bestellung konnte nicht abgeschlossen werden", "stripeNotLoaded": "Stripe wurde noch nicht geladen", "paymentProcessingError": "Bei der Zahlung ist ein Fehler aufgetreten.", - "failedToCompletePaymentSession": "Zahlungssitzung konnte nicht abgeschlossen werden" + "failedToCompletePaymentSession": "Zahlungssitzung konnte nicht abgeschlossen werden", + "noOrder": "Keine Bestellung" }, "address": { "firstName": "Vorname", @@ -211,7 +213,10 @@ "editAddress": "Adresse bearbeiten", "addNewAddress": "Neue Adresse hinzufügen", "saveAddress": "Adresse speichern", - "failedToSave": "Adresse konnte nicht gespeichert werden" + "failedToSave": "Adresse konnte nicht gespeichert werden", + "deleteAddressTitle": "Adresse löschen?", + "delete": "Löschen", + "deleting": "Wird gelöscht..." }, "account": { "myAccount": "Mein Konto", @@ -254,7 +259,10 @@ "passwordTooShort": "Das Passwort muss mindestens 6 Zeichen lang sein", "registrationFailed": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.", "alreadyHaveAccount": "Haben Sie bereits ein Konto?", - "signIn": "Anmelden" + "signIn": "Anmelden", + "unexpectedError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.", + "firstName": "Vorname", + "lastName": "Nachname" }, "orders": { "orderHistory": "Bestellverlauf", @@ -300,7 +308,9 @@ "creditOwed": "Guthaben geschuldet", "failed": "Fehlgeschlagen", "void": "Storniert", - "notAvailable": "k. A." + "notAvailable": "k. A.", + "orderTitle": "Bestellung #{number}", + "shipmentCanceledRefund": "Sendung storniert — eine Rückerstattung wurde veranlasst." }, "orderPlaced": { "thanksForOrder": "Vielen Dank für Ihre Bestellung, {name}!", @@ -312,7 +322,8 @@ "shippingAddress": "Lieferadresse", "billingAddress": "Rechnungsadresse", "orderNotFound": "Bestellung nicht gefunden", - "failedToLoad": "Bestelldetails konnten nicht geladen werden." + "failedToLoad": "Bestelldetails konnten nicht geladen werden.", + "qty": "Menge: {quantity}" }, "coupon": { "placeholder": "Geschenkkarte oder Rabattcode", @@ -342,7 +353,8 @@ "accountInformation": "Kontoinformationen", "accountId": "Konto-ID", "loadingProfile": "Profil wird geladen...", - "failedToUpdate": "Profil konnte nicht aktualisiert werden" + "failedToUpdate": "Profil konnte nicht aktualisiert werden", + "email": "E-Mail" }, "creditCards": { "paymentMethods": "Zahlungsmethoden", @@ -353,7 +365,9 @@ "removing": "Wird entfernt...", "secureInfo": "Ihre Zahlungsinformationen werden sicher gespeichert.", "deleteConfirm": "Sind Sie sicher, dass Sie diese Karte entfernen möchten?", - "cardMaskedLabel": "{label} endet auf {digits}, gültig bis {month}/{year}" + "cardMaskedLabel": "{label} endet auf {digits}, gültig bis {month}/{year}", + "removePaymentMethodTitle": "Zahlungsmethode entfernen?", + "endingIn": "endet auf" }, "giftCards": { "giftCards": "Geschenkkarten", diff --git a/messages/en.json b/messages/en.json index ec05253f..6fad8f8e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -37,7 +37,7 @@ "copyright": "© {year} {storeName}. Powered by Spree Commerce." }, "home": { - "welcome": "Welcome to Spree Store", + "welcome": "Welcome to {storeName}", "heroDescription": "Discover amazing products with our modern e-commerce experience powered by Spree Commerce.", "shopNow": "Shop Now", "browseCategories": "Browse Categories", @@ -64,7 +64,8 @@ "shoppingCart": "Shopping Cart", "proceedToCheckout": "Proceed to Checkout", "itemCount": "{count, plural, one {# item} other {# items}}", - "shippingCalculatedAtCheckout": "Calculated at checkout" + "shippingCalculatedAtCheckout": "Calculated at checkout", + "removeItemLabel": "Remove {name}" }, "products": { "allProducts": "All Products", @@ -189,7 +190,8 @@ "failedToCompleteOrder": "Failed to complete order", "stripeNotLoaded": "Stripe has not loaded yet", "paymentProcessingError": "An error occurred during payment.", - "failedToCompletePaymentSession": "Failed to complete payment session" + "failedToCompletePaymentSession": "Failed to complete payment session", + "noOrder": "No order" }, "address": { "firstName": "First name", @@ -211,7 +213,10 @@ "editAddress": "Edit Address", "addNewAddress": "Add New Address", "saveAddress": "Save Address", - "failedToSave": "Failed to save address" + "failedToSave": "Failed to save address", + "deleteAddressTitle": "Delete address?", + "delete": "Delete", + "deleting": "Deleting..." }, "account": { "myAccount": "My Account", @@ -254,7 +259,10 @@ "passwordTooShort": "Password must be at least 6 characters", "registrationFailed": "Registration failed. Please try again.", "alreadyHaveAccount": "Already have an account?", - "signIn": "Sign in" + "signIn": "Sign in", + "unexpectedError": "An unexpected error occurred. Please try again.", + "firstName": "First name", + "lastName": "Last name" }, "orders": { "orderHistory": "Order History", @@ -300,7 +308,9 @@ "creditOwed": "Credit Owed", "failed": "Failed", "void": "Void", - "notAvailable": "N/A" + "notAvailable": "N/A", + "orderTitle": "Order #{number}", + "shipmentCanceledRefund": "Shipment canceled — a refund has been issued." }, "orderPlaced": { "thanksForOrder": "Thanks for your order, {name}!", @@ -312,7 +322,8 @@ "shippingAddress": "Shipping Address", "billingAddress": "Billing Address", "orderNotFound": "Order not found", - "failedToLoad": "Failed to load order details." + "failedToLoad": "Failed to load order details.", + "qty": "Qty: {quantity}" }, "coupon": { "placeholder": "Gift card or discount code", @@ -342,7 +353,8 @@ "accountInformation": "Account Information", "accountId": "Account ID", "loadingProfile": "Loading profile...", - "failedToUpdate": "Failed to update profile" + "failedToUpdate": "Failed to update profile", + "email": "Email" }, "creditCards": { "paymentMethods": "Payment Methods", @@ -353,7 +365,9 @@ "removing": "Removing...", "secureInfo": "Your payment information is securely stored.", "deleteConfirm": "Are you sure you want to remove this card?", - "cardMaskedLabel": "{label} ending in {digits}, expires {month}/{year}" + "cardMaskedLabel": "{label} ending in {digits}, expires {month}/{year}", + "removePaymentMethodTitle": "Remove payment method?", + "endingIn": "ending in" }, "giftCards": { "giftCards": "Gift Cards", diff --git a/messages/pl.json b/messages/pl.json index 424ff17e..f907b673 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -37,7 +37,7 @@ "copyright": "© {year} {storeName}. Zasilany przez Spree Commerce." }, "home": { - "welcome": "Witaj w Sklepie Spree", + "welcome": "Witaj w {storeName}", "heroDescription": "Odkryj niesamowite produkty dzięki nowoczesnemu sklepowi internetowemu opartemu na Spree Commerce.", "shopNow": "Kup teraz", "browseCategories": "Przeglądaj kategorie", @@ -64,7 +64,8 @@ "shoppingCart": "Koszyk zakupowy", "proceedToCheckout": "Przejdź do kasy", "itemCount": "{count, plural, one {# produkt} few {# produkty} many {# produktów} other {# produktów}}", - "shippingCalculatedAtCheckout": "Obliczona przy kasie" + "shippingCalculatedAtCheckout": "Obliczona przy kasie", + "removeItemLabel": "Usuń {name}" }, "products": { "allProducts": "Wszystkie produkty", @@ -189,7 +190,8 @@ "failedToCompleteOrder": "Nie udało się złożyć zamówienia", "stripeNotLoaded": "Stripe nie został jeszcze załadowany", "paymentProcessingError": "Wystąpił błąd podczas przetwarzania płatności.", - "failedToCompletePaymentSession": "Nie udało się zakończyć sesji płatności" + "failedToCompletePaymentSession": "Nie udało się zakończyć sesji płatności", + "noOrder": "Brak zamówienia" }, "address": { "firstName": "Imię", @@ -211,7 +213,10 @@ "editAddress": "Edytuj adres", "addNewAddress": "Dodaj nowy adres", "saveAddress": "Zapisz adres", - "failedToSave": "Nie udało się zapisać adresu" + "failedToSave": "Nie udało się zapisać adresu", + "deleteAddressTitle": "Usunąć adres?", + "delete": "Usuń", + "deleting": "Usuwanie..." }, "account": { "myAccount": "Moje konto", @@ -254,7 +259,10 @@ "passwordTooShort": "Hasło musi mieć co najmniej 6 znaków", "registrationFailed": "Rejestracja nie powiodła się. Spróbuj ponownie.", "alreadyHaveAccount": "Masz już konto?", - "signIn": "Zaloguj się" + "signIn": "Zaloguj się", + "unexpectedError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.", + "firstName": "Imię", + "lastName": "Nazwisko" }, "orders": { "orderHistory": "Historia zamówień", @@ -300,7 +308,9 @@ "creditOwed": "Należny kredyt", "failed": "Nieudane", "void": "Unieważnione", - "notAvailable": "Brak" + "notAvailable": "Brak", + "orderTitle": "Zamówienie #{number}", + "shipmentCanceledRefund": "Przesyłka anulowana — zwrot został wystawiony." }, "orderPlaced": { "thanksForOrder": "Dziękujemy za zamówienie, {name}!", @@ -312,7 +322,8 @@ "shippingAddress": "Adres wysyłki", "billingAddress": "Adres rozliczeniowy", "orderNotFound": "Zamówienie nie zostało znalezione", - "failedToLoad": "Nie udało się załadować szczegółów zamówienia." + "failedToLoad": "Nie udało się załadować szczegółów zamówienia.", + "qty": "Ilość: {quantity}" }, "coupon": { "placeholder": "Karta podarunkowa lub kod rabatowy", @@ -342,7 +353,8 @@ "accountInformation": "Informacje o koncie", "accountId": "ID konta", "loadingProfile": "Ładowanie profilu...", - "failedToUpdate": "Nie udało się zaktualizować profilu" + "failedToUpdate": "Nie udało się zaktualizować profilu", + "email": "E-mail" }, "creditCards": { "paymentMethods": "Metody płatności", @@ -353,7 +365,9 @@ "removing": "Usuwanie...", "secureInfo": "Twoje dane płatnicze są bezpiecznie przechowywane.", "deleteConfirm": "Czy na pewno chcesz usunąć tę kartę?", - "cardMaskedLabel": "{label} kończąca się na {digits}, ważna do {month}/{year}" + "cardMaskedLabel": "{label} kończąca się na {digits}, ważna do {month}/{year}", + "removePaymentMethodTitle": "Usunąć metodę płatności?", + "endingIn": "kończąca się na" }, "giftCards": { "giftCards": "Karty podarunkowe", diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx index 2af87db0..1d0b6b12 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx @@ -88,6 +88,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const basePath = extractBasePath(pathname); const { setSummaryContent } = useCheckout(); const t = useTranslations("checkout"); + const tc = useTranslations("common"); const stepLabels: Record = { address: t("stepShipping"), @@ -113,28 +114,34 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const beginCheckoutFiredRef = useRef(false); // Handle coupon code application - uses ref to avoid stale closures - const handleApplyCoupon = useCallback(async (code: string) => { - const currentOrder = orderRef.current; - if (!currentOrder) return { success: false, error: "No order" }; + const handleApplyCoupon = useCallback( + async (code: string) => { + const currentOrder = orderRef.current; + if (!currentOrder) return { success: false, error: t("noOrder") }; - const result = await applyCouponCode(currentOrder.id, code); - if (result.success && result.order) { - setOrder(result.order); - } - return result; - }, []); // No dependencies - uses ref + const result = await applyCouponCode(currentOrder.id, code); + if (result.success && result.order) { + setOrder(result.order); + } + return result; + }, + [t], + ); // t is stable from useTranslations // Handle coupon code removal - const handleRemoveCoupon = useCallback(async (promotionId: string) => { - const currentOrder = orderRef.current; - if (!currentOrder) return { success: false, error: "No order" }; + const handleRemoveCoupon = useCallback( + async (promotionId: string) => { + const currentOrder = orderRef.current; + if (!currentOrder) return { success: false, error: t("noOrder") }; - const result = await removeCouponCode(currentOrder.id, promotionId); - if (result.success && result.order) { - setOrder(result.order); - } - return result; - }, []); // No dependencies - uses ref + const result = await removeCouponCode(currentOrder.id, promotionId); + if (result.success && result.order) { + setOrder(result.order); + } + return result; + }, + [t], + ); // t is stable from useTranslations // Track order key for sidebar updates (only update when order changes meaningfully) const orderKey = order ? `${order.id}-${order.updated_at}` : null; @@ -259,7 +266,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { }); if (!updateResult.success) { - setError(updateResult.error || "Failed to save address"); + setError(updateResult.error || t("failedToSaveAddress")); setProcessing(false); return; } @@ -267,7 +274,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // Move to next checkout step const nextResult = await nextCheckoutStep(order.id); if (!nextResult.success) { - setError(nextResult.error || "Failed to proceed to next step"); + setError(nextResult.error || t("failedToProceed")); setProcessing(false); return; } @@ -275,7 +282,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // Reload order to get updated state await loadOrder(); } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); } finally { setProcessing(false); } @@ -297,7 +304,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { try { const result = await selectShippingRate(order.id, shipmentId, rateId); if (!result.success) { - setError(result.error || "Failed to select shipping rate"); + setError(result.error || t("failedToSelectRate")); } else if (result.order) { setOrder(result.order); setShipments(result.order.shipments || []); @@ -309,7 +316,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { trackingRateName = selectedRate?.name; } } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); } finally { setProcessing(false); } @@ -334,7 +341,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // Move to next checkout step const nextResult = await nextCheckoutStep(order.id); if (!nextResult.success) { - setError(nextResult.error || "Failed to proceed"); + setError(nextResult.error || t("failedToProceed")); setProcessing(false); return; } @@ -342,7 +349,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // Reload order await loadOrder(); } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); } finally { setProcessing(false); } @@ -362,13 +369,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { }); if (!updateResult.success) { - setError(updateResult.error || "Failed to save billing address"); + setError(updateResult.error || t("failedToSaveBilling")); return false; } return true; } catch { - setError("Failed to save billing address. Please try again."); + setError(t("failedToSaveBillingRetry")); return false; } }; @@ -387,7 +394,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { ); if (!sessionResult.success) { - setError(sessionResult.error || "Failed to complete payment session"); + setError(sessionResult.error || t("failedToCompletePaymentSession")); setProcessing(false); return; } @@ -403,7 +410,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const updatedOrder = await getCheckoutOrder(order.id); if (!updatedOrder) { - setError("Order not found after payment. Please contact support."); + setError(t("orderNotFoundAfterPayment")); setProcessing(false); return; } @@ -411,7 +418,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { if (updatedOrder.current_step !== "complete") { const completeResult = await completeCheckoutOrder(order.id); if (!completeResult.success) { - setError(completeResult.error || "Failed to complete order"); + setError(completeResult.error || t("failedToCompleteOrder")); setProcessing(false); return; } @@ -420,7 +427,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // Redirect to order placed page (cart cookie is cleared there) router.push(`${basePath}/order-placed/${order.id}`); } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); setProcessing(false); } }; @@ -443,11 +450,11 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const result = await updateAddress(id, data); if (!result.success) { - throw new Error(result.error || "Failed to update address"); + throw new Error(result.error || t("failedToSaveAddress")); } if (!result.address) { - throw new Error("Update succeeded but address payload is missing"); + throw new Error(t("generalError")); } const updatedAddress = result.address; @@ -510,7 +517,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { href={`${basePath}/products`} className="inline-flex items-center px-4 py-2 bg-primary text-white rounded-xl hover:bg-primary-700" > - {t("returnToCart")} + {tc("continueShopping")}
    ); diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index dfbfa292..2ced98fb 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -62,6 +62,8 @@ function MobileSummaryToggle() { type="button" onClick={() => setIsOpen(!isOpen)} className="w-full px-5 py-4 flex items-center justify-between text-left" + aria-expanded={isOpen} + aria-controls="checkout-summary-panel" > @@ -72,7 +74,7 @@ function MobileSummaryToggle() { /> {isOpen && ( -
    +
    )} diff --git a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx index 7f78942e..f96f485b 100644 --- a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx @@ -129,7 +129,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) {

    {customerName - ? t("thanksForOrder", { name: customerName.split(" ")[0] }) + ? t("thanksForOrder", { name: customerName }) : t("thanksForOrderAnonymous")}

    @@ -164,7 +164,9 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { {item.options_text && (

    {item.options_text}

    )} -

    Qty: {item.quantity}

    +

    + {t("qty", { quantity: item.quantity })} +

    {item.display_total} diff --git a/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx b/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx index 823ad496..4c092de3 100644 --- a/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx @@ -36,6 +36,7 @@ function AddressCard({ }) { const t = useTranslations("account"); const tc = useTranslations("common"); + const ta = useTranslations("address"); const [deleting, setDeleting] = useState(false); const handleDelete = async () => { @@ -71,20 +72,20 @@ function AddressCard({ - Delete address? + {ta("deleteAddressTitle")} {t("deleteConfirm")} - Cancel + {tc("cancel")} - Delete + {ta("delete")} diff --git a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx index 5cf10c9c..bce307b0 100644 --- a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx @@ -51,7 +51,7 @@ function CreditCardItem({ />

    - {getCardLabel(card.cc_type)} ending in {card.last_digits} + {getCardLabel(card.cc_type)} {t("endingIn")} {card.last_digits}

    {t("expires")} {String(card.month).padStart(2, "0")}/{card.year} @@ -75,13 +75,15 @@ function CreditCardItem({ - Remove payment method? + + {t("removePaymentMethodTitle")} + {t("deleteConfirm")} - Cancel + {tc("cancel")} {tc("remove")} diff --git a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx index 6d2a019d..33fb3e96 100644 --- a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx @@ -2,7 +2,7 @@ import type { GiftCard } from "@spree/sdk"; import { Check, ClipboardCopy, Gift, Info } from "lucide-react"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { getGiftCards } from "@/lib/data/gift-cards"; @@ -23,10 +23,10 @@ function getStateColor(state: string, expired: boolean): string { } } -function formatDate(dateString: string | null): string { +function formatDate(dateString: string | null, locale: string): string { if (!dateString) return ""; const date = new Date(dateString); - return date.toLocaleDateString("en-US", { + return date.toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric", @@ -60,7 +60,7 @@ function CopyButton({ code }: { code: string }) { variant="ghost" size="sm" onClick={handleCopy} - title="Copy code to clipboard" + title={t("copyCodeToClipboard")} > {copied ? ( <> @@ -79,6 +79,7 @@ function CopyButton({ code }: { code: string }) { function GiftCardItem({ card }: { card: GiftCard }) { const t = useTranslations("giftCards"); + const locale = useLocale(); const usagePercentage = Number(card.amount) > 0 ? Math.round((Number(card.amount_used) / Number(card.amount)) * 100) @@ -118,7 +119,7 @@ function GiftCardItem({ card }: { card: GiftCard }) {

    {card.expires_at - ? t("expiresOn", { date: formatDate(card.expires_at) }) + ? t("expiresOn", { date: formatDate(card.expires_at, locale) }) : t("noExpiration")}

    @@ -156,9 +157,15 @@ function GiftCardItem({ card }: { card: GiftCard }) { {/* Additional info */}
    - {t("addedOnDate", { date: formatDate(card.created_at) })} + + {t("addedOnDate", { date: formatDate(card.created_at, locale) })} + {card.redeemed_at && ( - Fully redeemed on {formatDate(card.redeemed_at)} + + {t("fullyRedeemedOnDate", { + date: formatDate(card.redeemed_at, locale), + })} + )}
    @@ -255,8 +262,7 @@ export default function GiftCardsPage() {

    - Gift cards can be used during checkout to pay for your orders. The - remaining balance will be saved for future purchases. + {t("giftCardsHelpText")}

    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index a20fb91e..900dfb18 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -11,7 +11,7 @@ import type { import { ChevronLeft, CircleAlert } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import { use, useEffect, useState } from "react"; import { PaymentIcon } from "react-svg-credit-card-payment-icons"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -19,11 +19,15 @@ import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; import { getOrder } from "@/lib/data/orders"; import { getCardIconType, getCardLabel } from "@/lib/utils/credit-card"; +import { + getShipmentStatusColor, + SHIPMENT_STATE_KEY, +} from "@/lib/utils/order-status"; import { extractBasePath } from "@/lib/utils/path"; -function formatDate(dateString: string | null): string { +function formatDate(dateString: string | null, locale: string): string { if (!dateString) return "-"; - return new Date(dateString).toLocaleDateString("en-US", { + return new Date(dateString).toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", @@ -32,21 +36,6 @@ function formatDate(dateString: string | null): string { }); } -function getShipmentStatusColor(state: string): string { - switch (state) { - case "shipped": - case "delivered": - return "bg-green-100 text-green-800"; - case "ready": - case "pending": - return "bg-yellow-100 text-yellow-800"; - case "canceled": - return "bg-red-100 text-red-800"; - default: - return "bg-gray-100 text-gray-800"; - } -} - function PaymentSourceInfo({ payment }: { payment: Payment }) { const t = useTranslations("orders"); const source = payment.source; @@ -84,8 +73,10 @@ function PaymentSourceInfo({ payment }: { payment: Payment }) {

    {t("storeCredit")}

    - Applied {payment.display_amount} — {credit.display_amount_remaining}{" "} - remaining + {t("storeCreditApplied", { + amount: payment.display_amount, + remaining: credit.display_amount_remaining, + })}

    ); @@ -163,7 +154,7 @@ function LineItemCard({ href={`${basePath}/products/${item.slug}`} className="mt-2 inline-block text-sm text-primary hover:text-primary font-medium" > - Order again + {t("orderAgain")}
    @@ -206,17 +197,17 @@ function ShipmentBlock({ {t("shippingMethod")}

    - {shipment.shipping_method?.name || "Canceled"} + {shipment.shipping_method?.name || t("canceled")}

    {shipment.stock_location && (

    - Shipped from {shipment.stock_location.name} + {t("shippedFrom", { location: shipment.stock_location.name })}

    )} - {shipment.state} + {t(SHIPMENT_STATE_KEY[shipment.state] || shipment.state)}
    @@ -242,16 +233,14 @@ function ShipmentBlock({ {shipment.state === "canceled" && !shipment.shipped_at && ( - - Shipment canceled — a refund has been issued. - + {t("shipmentCanceledRefund")} )} {shipment.state !== "canceled" && shipment.state !== "shipped" && !shipment.tracking && (
    - No tracking information present + {t("noTrackingInfo")}
    )}
    @@ -279,6 +268,7 @@ interface OrderDetailPageProps { export default function OrderDetailPage({ params }: OrderDetailPageProps) { const t = useTranslations("orders"); const tc = useTranslations("common"); + const locale = useLocale(); const { id } = use(params); const pathname = usePathname(); const basePath = extractBasePath(pathname); @@ -313,9 +303,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) {

    {t("orderNotFound")}

    -

    - The order you're looking for doesn't exist. -

    +

    {t("orderNotFoundDescription")}

    - Order #{order.number} + {t("orderTitle", { number: order.number })}

    - {t("placedOn", { date: formatDate(order.completed_at) })} + {t("placedOn", { date: formatDate(order.completed_at, locale) })}

    {/* Shipments with line items */} diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx index bd411258..5a4a58b9 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -8,6 +8,12 @@ import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { getOrders } from "@/lib/data/orders"; +import { + getPaymentStatusColor, + getShipmentStatusColor, + PAYMENT_STATE_KEY, + SHIPMENT_STATE_KEY, +} from "@/lib/utils/order-status"; import { extractBasePath } from "@/lib/utils/path"; function formatDate(dateString: string | null, locale: string): string { @@ -19,55 +25,6 @@ function formatDate(dateString: string | null, locale: string): string { }); } -const PAYMENT_STATE_KEY: Record = { - paid: "paid", - balance_due: "balanceDue", - credit_owed: "creditOwed", - pending: "pending", - failed: "failed", - void: "void", -}; - -const SHIPMENT_STATE_KEY: Record = { - shipped: "shipped", - partial: "partiallyShipped", - delivered: "delivered", - ready: "ready", - pending: "pending", - canceled: "canceled", - backorder: "backorder", -}; - -function getPaymentStatusColor(state: string | null): string { - switch (state) { - case "paid": - return "bg-green-100 text-green-800"; - case "balance_due": - case "pending": - return "bg-yellow-100 text-yellow-800"; - case "failed": - case "void": - return "bg-red-100 text-red-800"; - default: - return "bg-gray-100 text-gray-800"; - } -} - -function getShipmentStatusColor(state: string | null): string { - switch (state) { - case "shipped": - case "delivered": - return "bg-green-100 text-green-800"; - case "ready": - case "pending": - return "bg-yellow-100 text-yellow-800"; - case "canceled": - return "bg-red-100 text-red-800"; - default: - return "bg-gray-100 text-gray-800"; - } -} - export default function OrdersPage() { const t = useTranslations("orders"); const tc = useTranslations("common"); diff --git a/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx b/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx index 78930ffc..5ffad034 100644 --- a/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/profile/page.tsx @@ -128,7 +128,9 @@ function ProfileForm({
    {user.id}
    -
    Email
    +
    + {t("email")} +
    {user.email}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx index 4be9a094..b61f4548 100644 --- a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx @@ -72,7 +72,7 @@ export default function RegisterPage() { setError(result.error || t("registrationFailed")); } } catch { - setError("An unexpected error occurred. Please try again."); + setError(t("unexpectedError")); } finally { setSubmitting(false); } @@ -98,7 +98,7 @@ export default function RegisterPage() {
    - First name + {t("firstName")} - Last name + {t("lastName")} handleRemove(item)} > {tc("remove")} diff --git a/src/app/[country]/[locale]/(storefront)/page.tsx b/src/app/[country]/[locale]/(storefront)/page.tsx index fa018aca..964a957b 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server"; import { ProductCarousel } from "@/components/products/ProductCarousel"; import { Button } from "@/components/ui/button"; import { generateHomeMetadata } from "@/lib/metadata/home"; +import { getStoreName } from "@/lib/seo"; interface HomePageProps { params: Promise<{ @@ -23,6 +24,7 @@ export default async function HomePage({ params }: HomePageProps) { const { country, locale } = await params; const basePath = `/${country}/${locale}`; const t = await getTranslations("home"); + const storeName = getStoreName(); return (
    @@ -31,7 +33,7 @@ export default async function HomePage({ params }: HomePageProps) {

    - {t("welcome")} + {t("welcome", { storeName })}

    {t("heroDescription")} @@ -41,7 +43,7 @@ export default async function HomePage({ params }: HomePageProps) { {t("shopNow")} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 693d6f28..7a16763c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,8 @@ import { GoogleTagManager } from "@next/third-parties/google"; import type { Metadata } from "next"; import { Geist } from "next/font/google"; -import { cookies } from "next/headers"; import { NextIntlClientProvider } from "next-intl"; -import { getMessages } from "next-intl/server"; +import { getLocale, getMessages } from "next-intl/server"; import "./globals.css"; import { CartDrawer } from "@/components/cart/CartDrawer"; import { Toaster } from "@/components/ui/sonner"; @@ -32,8 +31,7 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const cookieStore = await cookies(); - const locale = cookieStore.get("spree_locale")?.value || "en"; + const locale = await getLocale(); const messages = await getMessages(); return ( diff --git a/src/components/checkout/AddressSelector.tsx b/src/components/checkout/AddressSelector.tsx index cfae53ac..e2fc6e36 100644 --- a/src/components/checkout/AddressSelector.tsx +++ b/src/components/checkout/AddressSelector.tsx @@ -80,10 +80,10 @@ export function AddressSelector({

    {/* Saved addresses selection */} {savedAddresses.length > 0 && ( -
    -

    +

    + {t("selectAnAddress")} -

    +
    {savedAddresses.map((address) => (
    -
    +
    )} {/* Address form (shown when "new" is selected or no saved addresses) */} diff --git a/src/components/navigation/Breadcrumbs.tsx b/src/components/navigation/Breadcrumbs.tsx index 6587c176..366822aa 100644 --- a/src/components/navigation/Breadcrumbs.tsx +++ b/src/components/navigation/Breadcrumbs.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { Category } from "@spree/sdk"; import { ChevronRight } from "lucide-react"; import Link from "next/link"; diff --git a/src/components/products/ProductCarousel.tsx b/src/components/products/ProductCarousel.tsx index 894f48de..d1df21ff 100644 --- a/src/components/products/ProductCarousel.tsx +++ b/src/components/products/ProductCarousel.tsx @@ -71,7 +71,7 @@ export function ProductCarousel({ if (products.length === 0) { return (
    -

    No products found.

    +

    {t("noProductsFound")}

    ); } diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 823e6449..5dfe440a 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -167,7 +167,7 @@ export function SearchBar({ basePath }: SearchBarProps) { selectedIndex >= 0 ? `search-option-${selectedIndex}` : undefined } aria-autocomplete="list" - aria-label="Search" + aria-label={t("search")} /> diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 00178cb6..252aec2e 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -8,13 +8,16 @@ export default getRequestConfig(async ({ requestLocale }) => { // fall back to the spree_locale cookie, then default to "en". let locale = await requestLocale; - if (!locale || !supportedLocales.includes(locale)) { + if (locale && !supportedLocales.includes(locale)) { + // Invalid locale from route — default to "en" directly + locale = "en"; + } else if (!locale) { + // No locale from route — try cookie, then default to "en" const cookieStore = await cookies(); locale = cookieStore.get("spree_locale")?.value; - } - - if (!locale || !supportedLocales.includes(locale)) { - locale = "en"; + if (!locale || !supportedLocales.includes(locale)) { + locale = "en"; + } } let messages: IntlMessages; diff --git a/src/lib/utils/order-status.ts b/src/lib/utils/order-status.ts new file mode 100644 index 00000000..2958c297 --- /dev/null +++ b/src/lib/utils/order-status.ts @@ -0,0 +1,53 @@ +/** + * Maps raw Spree payment/shipment state strings to i18n translation keys + * under the "orders" namespace. + */ + +export const PAYMENT_STATE_KEY: Record = { + paid: "paid", + balance_due: "balanceDue", + credit_owed: "creditOwed", + pending: "pending", + failed: "failed", + void: "void", +}; + +export const SHIPMENT_STATE_KEY: Record = { + shipped: "shipped", + partial: "partiallyShipped", + delivered: "delivered", + ready: "ready", + pending: "pending", + canceled: "canceled", + backorder: "backorder", +}; + +export function getPaymentStatusColor(state: string | null): string { + switch (state) { + case "paid": + return "bg-green-100 text-green-800"; + case "balance_due": + case "pending": + return "bg-yellow-100 text-yellow-800"; + case "failed": + case "void": + return "bg-red-100 text-red-800"; + default: + return "bg-gray-100 text-gray-800"; + } +} + +export function getShipmentStatusColor(state: string | null): string { + switch (state) { + case "shipped": + case "delivered": + return "bg-green-100 text-green-800"; + case "ready": + case "pending": + return "bg-yellow-100 text-yellow-800"; + case "canceled": + return "bg-red-100 text-red-800"; + default: + return "bg-gray-100 text-gray-800"; + } +} From be133dcd59548f4fbcfb1c6d27d0aad11b34c6cb Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Thu, 12 Mar 2026 13:29:17 +0100 Subject: [PATCH 03/28] Add @swc/helpers as direct dependency to fix CI lockfile sync @swc/core from next-intl requires @swc/helpers >=0.5.17 but the lockfile only had 0.5.15 via next. Adding it explicitly at 0.5.19 ensures npm ci resolves correctly. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 17 ++++++++++++++--- package.json | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9886bbc..bad4d689 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@spree/sdk": "^0.9.0", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", + "@swc/helpers": "^0.5.19", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", @@ -6078,10 +6079,11 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -10360,6 +10362,15 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index 996d7a87..d68e62fd 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@spree/sdk": "^0.9.0", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", + "@swc/helpers": "^0.5.19", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", From a9d8024d2d85dca1218ec27b1567bb76449f573c Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Thu, 12 Mar 2026 13:35:38 +0100 Subject: [PATCH 04/28] Use parameterized translation keys for credit card labels Replace concatenated translation fragments with cardEndingIn and cardExpires keys so translators can control word order per locale. Co-Authored-By: Claude Opus 4.6 --- messages/de.json | 4 +++- messages/en.json | 4 +++- messages/pl.json | 4 +++- .../(storefront)/account/credit-cards/page.tsx | 10 ++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/messages/de.json b/messages/de.json index 5c8af28f..7a0904fc 100644 --- a/messages/de.json +++ b/messages/de.json @@ -367,7 +367,9 @@ "deleteConfirm": "Sind Sie sicher, dass Sie diese Karte entfernen möchten?", "cardMaskedLabel": "{label} endet auf {digits}, gültig bis {month}/{year}", "removePaymentMethodTitle": "Zahlungsmethode entfernen?", - "endingIn": "endet auf" + "endingIn": "endet auf", + "cardEndingIn": "{label} endet auf {digits}", + "cardExpires": "Gültig bis {month}/{year}" }, "giftCards": { "giftCards": "Geschenkkarten", diff --git a/messages/en.json b/messages/en.json index 6fad8f8e..cee8f181 100644 --- a/messages/en.json +++ b/messages/en.json @@ -367,7 +367,9 @@ "deleteConfirm": "Are you sure you want to remove this card?", "cardMaskedLabel": "{label} ending in {digits}, expires {month}/{year}", "removePaymentMethodTitle": "Remove payment method?", - "endingIn": "ending in" + "endingIn": "ending in", + "cardEndingIn": "{label} ending in {digits}", + "cardExpires": "Expires {month}/{year}" }, "giftCards": { "giftCards": "Gift Cards", diff --git a/messages/pl.json b/messages/pl.json index f907b673..0db864e1 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -367,7 +367,9 @@ "deleteConfirm": "Czy na pewno chcesz usunąć tę kartę?", "cardMaskedLabel": "{label} kończąca się na {digits}, ważna do {month}/{year}", "removePaymentMethodTitle": "Usunąć metodę płatności?", - "endingIn": "kończąca się na" + "endingIn": "kończąca się na", + "cardEndingIn": "{label} kończąca się na {digits}", + "cardExpires": "Ważna do {month}/{year}" }, "giftCards": { "giftCards": "Karty podarunkowe", diff --git a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx index bce307b0..fc3135f3 100644 --- a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx @@ -51,10 +51,16 @@ function CreditCardItem({ />

    - {getCardLabel(card.cc_type)} {t("endingIn")} {card.last_digits} + {t("cardEndingIn", { + label: getCardLabel(card.cc_type), + digits: card.last_digits, + })}

    - {t("expires")} {String(card.month).padStart(2, "0")}/{card.year} + {t("cardExpires", { + month: String(card.month).padStart(2, "0"), + year: card.year, + })}

    {card.name && (

    {card.name}

    From 092044090563e5ddcb4c5d2374ad622b60182f12 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Fri, 13 Mar 2026 13:15:39 +0100 Subject: [PATCH 05/28] Remove unused Order import from PaymentStep after SDK 0.9 rebase Co-Authored-By: Claude Opus 4.6 --- src/components/checkout/PaymentStep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/checkout/PaymentStep.tsx b/src/components/checkout/PaymentStep.tsx index f3d287a9..9a544f67 100644 --- a/src/components/checkout/PaymentStep.tsx +++ b/src/components/checkout/PaymentStep.tsx @@ -4,7 +4,6 @@ import type { AddressParams, Cart, Country, - Order, CreditCard as SpreeCreditCard, State, } from "@spree/sdk"; From 8697d1709ad61d8dc213128613bffddb45d2c600 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Fri, 13 Mar 2026 13:56:32 +0100 Subject: [PATCH 06/28] Fix code review issues: expired label, fallback translations, error handling, a11y - Gift cards: getStateLabel now checks expired flag to match getStateColor - Order detail: shipment state fallback uses translation key instead of raw API value - Homepage: fix broken /categories link to point to /products - Orders page: add try/catch/finally to loadOrders to prevent stuck loading state - Checkout layout: make aria-controls conditional on panel visibility - Account layout: rename useNavItems to getNavItems (not a hook) Co-Authored-By: Claude Opus 4.6 --- messages/de.json | 1 + messages/en.json | 1 + messages/pl.json | 1 + src/app/[country]/[locale]/(checkout)/layout.tsx | 2 +- .../(storefront)/account/gift-cards/page.tsx | 5 +++-- .../[locale]/(storefront)/account/layout.tsx | 4 ++-- .../(storefront)/account/orders/[id]/page.tsx | 4 +++- .../[locale]/(storefront)/account/orders/page.tsx | 13 +++++++++---- src/app/[country]/[locale]/(storefront)/page.tsx | 2 +- 9 files changed, 22 insertions(+), 11 deletions(-) diff --git a/messages/de.json b/messages/de.json index 7a0904fc..9209c431 100644 --- a/messages/de.json +++ b/messages/de.json @@ -309,6 +309,7 @@ "failed": "Fehlgeschlagen", "void": "Storniert", "notAvailable": "k. A.", + "unknownShipmentStatus": "Unbekannt", "orderTitle": "Bestellung #{number}", "shipmentCanceledRefund": "Sendung storniert — eine Rückerstattung wurde veranlasst." }, diff --git a/messages/en.json b/messages/en.json index cee8f181..05c2e370 100644 --- a/messages/en.json +++ b/messages/en.json @@ -309,6 +309,7 @@ "failed": "Failed", "void": "Void", "notAvailable": "N/A", + "unknownShipmentStatus": "Unknown", "orderTitle": "Order #{number}", "shipmentCanceledRefund": "Shipment canceled — a refund has been issued." }, diff --git a/messages/pl.json b/messages/pl.json index 0db864e1..bcf76e50 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -309,6 +309,7 @@ "failed": "Nieudane", "void": "Unieważnione", "notAvailable": "Brak", + "unknownShipmentStatus": "Nieznany", "orderTitle": "Zamówienie #{number}", "shipmentCanceledRefund": "Przesyłka anulowana — zwrot został wystawiony." }, diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index 2ced98fb..20869b9c 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -63,7 +63,7 @@ function MobileSummaryToggle() { onClick={() => setIsOpen(!isOpen)} className="w-full px-5 py-4 flex items-center justify-between text-left" aria-expanded={isOpen} - aria-controls="checkout-summary-panel" + {...(isOpen && { "aria-controls": "checkout-summary-panel" })} > diff --git a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx index 33fb3e96..bfe05c7c 100644 --- a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx @@ -85,7 +85,8 @@ function GiftCardItem({ card }: { card: GiftCard }) { ? Math.round((Number(card.amount_used) / Number(card.amount)) * 100) : 0; - function getStateLabel(state: string): string { + function getStateLabel(state: string, expired?: boolean): string { + if (expired) return t("expired"); switch (state) { case "active": return t("active"); @@ -114,7 +115,7 @@ function GiftCardItem({ card }: { card: GiftCard }) { - {getStateLabel(card.state)} + {getStateLabel(card.state, card.expired)}

    diff --git a/src/app/[country]/[locale]/(storefront)/account/layout.tsx b/src/app/[country]/[locale]/(storefront)/account/layout.tsx index 249c93b6..4cacaa0d 100644 --- a/src/app/[country]/[locale]/(storefront)/account/layout.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/layout.tsx @@ -18,7 +18,7 @@ import { Button } from "@/components/ui/button"; import { useAuth } from "@/contexts/AuthContext"; import { extractBasePath } from "@/lib/utils/path"; -function useNavItems(t: ReturnType>): { +function getNavItems(t: ReturnType>): { href: string; label: string; icon: LucideIcon; @@ -70,7 +70,7 @@ function AccountShell({ isLoading, }: AccountShellProps) { const t = useTranslations("account"); - const navItems = useNavItems(t); + const navItems = getNavItems(t); return (

    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index 900dfb18..c80064d7 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -207,7 +207,9 @@ function ShipmentBlock({ - {t(SHIPMENT_STATE_KEY[shipment.state] || shipment.state)} + {t( + SHIPMENT_STATE_KEY[shipment.state] || "unknownShipmentStatus", + )}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx index 5a4a58b9..60e2dd25 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -36,10 +36,15 @@ export default function OrdersPage() { useEffect(() => { async function loadOrders() { - const response = await getOrders({ limit: 50 }); - // Filter to only show completed orders - setOrders(response.data.filter((o) => o.completed_at !== null)); - setLoading(false); + try { + const response = await getOrders({ limit: 50 }); + // Filter to only show completed orders + setOrders(response.data.filter((o) => o.completed_at !== null)); + } catch { + setOrders([]); + } finally { + setLoading(false); + } } loadOrders(); }, []); diff --git a/src/app/[country]/[locale]/(storefront)/page.tsx b/src/app/[country]/[locale]/(storefront)/page.tsx index 964a957b..a1140604 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -43,7 +43,7 @@ export default async function HomePage({ params }: HomePageProps) { {t("shopNow")} From 4781628d6eacd370073cabfd70069966015e2131 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Mon, 16 Mar 2026 11:13:36 +0100 Subject: [PATCH 07/28] Add i18n translations to new pages from main merge Translate all hardcoded strings in 7 new/updated files from the main merge (one-page checkout, forgot/reset password, confirm payment). Add forgotPassword and resetPassword namespaces to all locales (en/pl/de) and sync missing checkout keys across translation files. Co-Authored-By: Claude Opus 4.6 --- messages/de.json | 45 +++++++++++++++-- messages/en.json | 45 +++++++++++++++-- messages/pl.json | 45 +++++++++++++++-- .../(checkout)/confirm-payment/[id]/page.tsx | 8 +-- .../account/forgot-password/page.tsx | 34 ++++++------- .../(storefront)/account/orders/[id]/page.tsx | 34 +++++-------- .../[locale]/(storefront)/account/page.tsx | 4 +- .../(storefront)/account/register/page.tsx | 8 +-- .../account/reset-password/page.tsx | 49 +++++++++---------- src/components/checkout/AddressSection.tsx | 23 ++++++--- src/components/checkout/PaymentSection.tsx | 45 ++++++++--------- .../checkout/ShippingMethodSection.tsx | 19 +++++-- src/components/checkout/Summary.tsx | 17 ++++--- 13 files changed, 252 insertions(+), 124 deletions(-) diff --git a/messages/de.json b/messages/de.json index 9209c431..fb4453a1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -162,6 +162,7 @@ "continueToDelivery": "Weiter zur Lieferung", "shippingMethod": "Versandmethode", "loadingShippingOptions": "Versandoptionen werden geladen...", + "enterShippingAddressForMethods": "Geben Sie Ihre Lieferadresse ein, um verfügbare Versandmethoden anzuzeigen.", "shipmentOf": "Sendung {current} von {total}", "shipsFrom": "Versand von {location}", "phone": "Telefon: {phone}", @@ -174,6 +175,9 @@ "exp": "Gültig bis", "default": "Standard", "noPaymentMethods": "Für diese Bestellung sind keine Zahlungsmethoden verfügbar.", + "secureTransactions": "Alle Transaktionen sind sicher und verschlüsselt.", + "creditCard": "Kreditkarte", + "enterShippingAddress": "Lieferadresse eingeben", "payNow": "Jetzt bezahlen", "failedToInitPayment": "Zahlung konnte nicht initialisiert werden. Bitte versuchen Sie es erneut.", "failedToCreateSession": "Zahlungssitzung konnte nicht erstellt werden.", @@ -191,7 +195,8 @@ "stripeNotLoaded": "Stripe wurde noch nicht geladen", "paymentProcessingError": "Bei der Zahlung ist ein Fehler aufgetreten.", "failedToCompletePaymentSession": "Zahlungssitzung konnte nicht abgeschlossen werden", - "noOrder": "Keine Bestellung" + "noOrder": "Keine Bestellung", + "confirmingPayment": "Ihre Zahlung wird bestätigt..." }, "address": { "firstName": "Vorname", @@ -248,7 +253,8 @@ "deleteConfirm": "Sind Sie sicher, dass Sie diese Adresse löschen möchten?", "setAsDefault": "Als Standard festlegen", "showPassword": "Passwort anzeigen", - "hidePassword": "Passwort verbergen" + "hidePassword": "Passwort verbergen", + "forgotPassword": "Passwort vergessen?" }, "register": { "createAccount": "Konto erstellen", @@ -311,7 +317,8 @@ "notAvailable": "k. A.", "unknownShipmentStatus": "Unbekannt", "orderTitle": "Bestellung #{number}", - "shipmentCanceledRefund": "Sendung storniert — eine Rückerstattung wurde veranlasst." + "shipmentCanceledRefund": "Sendung storniert — eine Rückerstattung wurde veranlasst.", + "shippingMethodUnavailable": "Nicht verfügbar" }, "orderPlaced": { "thanksForOrder": "Vielen Dank für Ihre Bestellung, {name}!", @@ -395,5 +402,37 @@ "expiresOn": "Läuft ab am {date}", "percentUsed": "{percent}% verwendet", "giftCardsHelpText": "Geschenkkarten können beim Checkout zum Bezahlen Ihrer Bestellungen verwendet werden. Das verbleibende Guthaben wird für zukünftige Einkäufe gespeichert." + }, + "forgotPassword": { + "title": "Passwort zurücksetzen", + "description": "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.", + "email": "E-Mail", + "sending": "Wird gesendet...", + "sendResetLink": "Link zum Zurücksetzen senden", + "backToSignIn": "Zurück zur Anmeldung", + "checkYourEmail": "Überprüfen Sie Ihre E-Mail", + "resetEmailSent": "Wenn ein Konto für {email} existiert, haben wir Anweisungen zum Zurücksetzen des Passworts gesendet.", + "linkExpiry": "Der Link in der E-Mail läuft in 15 Minuten ab. Überprüfen Sie Ihren Spam-Ordner, wenn Sie ihn nicht sehen.", + "tryDifferentEmail": "Andere E-Mail-Adresse versuchen", + "genericError": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut." + }, + "resetPassword": { + "title": "Neues Passwort festlegen", + "description": "Geben Sie unten Ihr neues Passwort ein.", + "newPassword": "Neues Passwort", + "confirmPassword": "Neues Passwort bestätigen", + "resetting": "Wird zurückgesetzt...", + "resetPassword": "Passwort zurücksetzen", + "backToSignIn": "Zurück zur Anmeldung", + "invalidLink": "Ungültiger Link", + "invalidLinkDescription": "Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen.", + "requestNewLink": "Neuen Link anfordern", + "passwordsDontMatch": "Passwörter stimmen nicht überein", + "passwordTooShort": "Das Passwort muss mindestens 6 Zeichen lang sein", + "linkExpired": "Dieser Link ist abgelaufen. Bitte fordern Sie einen neuen an.", + "genericError": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.", + "success": "Passwort erfolgreich zurückgesetzt", + "successDescription": "Ihr Passwort wurde aktualisiert. Sie können sich jetzt mit Ihrem neuen Passwort anmelden.", + "signIn": "Anmelden" } } diff --git a/messages/en.json b/messages/en.json index 05c2e370..6df82b92 100644 --- a/messages/en.json +++ b/messages/en.json @@ -162,6 +162,7 @@ "continueToDelivery": "Continue to Delivery", "shippingMethod": "Shipping Method", "loadingShippingOptions": "Loading shipping options...", + "enterShippingAddressForMethods": "Enter your shipping address to view available shipping methods.", "shipmentOf": "Shipment {current} of {total}", "shipsFrom": "Ships from {location}", "phone": "Phone: {phone}", @@ -174,6 +175,9 @@ "exp": "Exp", "default": "Default", "noPaymentMethods": "No payment methods available for this order.", + "secureTransactions": "All transactions are secure and encrypted.", + "creditCard": "Credit card", + "enterShippingAddress": "Enter shipping address", "payNow": "Pay Now", "failedToInitPayment": "Failed to initialize payment. Please try again.", "failedToCreateSession": "Failed to create payment session.", @@ -191,7 +195,8 @@ "stripeNotLoaded": "Stripe has not loaded yet", "paymentProcessingError": "An error occurred during payment.", "failedToCompletePaymentSession": "Failed to complete payment session", - "noOrder": "No order" + "noOrder": "No order", + "confirmingPayment": "Confirming your payment..." }, "address": { "firstName": "First name", @@ -248,7 +253,8 @@ "deleteConfirm": "Are you sure you want to delete this address?", "setAsDefault": "Set as default", "showPassword": "Show password", - "hidePassword": "Hide password" + "hidePassword": "Hide password", + "forgotPassword": "Forgot password?" }, "register": { "createAccount": "Create Account", @@ -311,7 +317,8 @@ "notAvailable": "N/A", "unknownShipmentStatus": "Unknown", "orderTitle": "Order #{number}", - "shipmentCanceledRefund": "Shipment canceled — a refund has been issued." + "shipmentCanceledRefund": "Shipment canceled — a refund has been issued.", + "shippingMethodUnavailable": "Unavailable" }, "orderPlaced": { "thanksForOrder": "Thanks for your order, {name}!", @@ -395,5 +402,37 @@ "expiresOn": "Expires {date}", "percentUsed": "{percent}% used", "giftCardsHelpText": "Gift cards can be used during checkout to pay for your orders. The remaining balance will be saved for future purchases." + }, + "forgotPassword": { + "title": "Reset your password", + "description": "Enter your email and we'll send you a link to reset your password.", + "email": "Email", + "sending": "Sending...", + "sendResetLink": "Send reset link", + "backToSignIn": "Back to sign in", + "checkYourEmail": "Check your email", + "resetEmailSent": "If an account exists for {email}, we've sent password reset instructions.", + "linkExpiry": "The link in the email will expire in 15 minutes. Check your spam folder if you don't see it.", + "tryDifferentEmail": "Try a different email", + "genericError": "Something went wrong. Please try again." + }, + "resetPassword": { + "title": "Set a new password", + "description": "Enter your new password below.", + "newPassword": "New password", + "confirmPassword": "Confirm new password", + "resetting": "Resetting...", + "resetPassword": "Reset password", + "backToSignIn": "Back to sign in", + "invalidLink": "Invalid link", + "invalidLinkDescription": "This password reset link is invalid or has expired.", + "requestNewLink": "Request a new reset link", + "passwordsDontMatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "linkExpired": "This reset link has expired. Please request a new one.", + "genericError": "Something went wrong. Please try again.", + "success": "Password reset successful", + "successDescription": "Your password has been updated. You can now sign in with your new password.", + "signIn": "Sign in" } } diff --git a/messages/pl.json b/messages/pl.json index bcf76e50..ac005fe6 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -162,6 +162,7 @@ "continueToDelivery": "Przejdź do dostawy", "shippingMethod": "Metoda wysyłki", "loadingShippingOptions": "Ładowanie opcji wysyłki...", + "enterShippingAddressForMethods": "Podaj adres wysyłki, aby zobaczyć dostępne metody dostawy.", "shipmentOf": "Przesyłka {current} z {total}", "shipsFrom": "Wysyłka z {location}", "phone": "Telefon: {phone}", @@ -174,6 +175,9 @@ "exp": "Ważna do", "default": "Domyślna", "noPaymentMethods": "Brak dostępnych metod płatności dla tego zamówienia.", + "secureTransactions": "Wszystkie transakcje są bezpieczne i szyfrowane.", + "creditCard": "Karta kredytowa", + "enterShippingAddress": "Podaj adres wysyłki", "payNow": "Zapłać teraz", "failedToInitPayment": "Nie udało się zainicjować płatności. Spróbuj ponownie.", "failedToCreateSession": "Nie udało się utworzyć sesji płatności.", @@ -191,7 +195,8 @@ "stripeNotLoaded": "Stripe nie został jeszcze załadowany", "paymentProcessingError": "Wystąpił błąd podczas przetwarzania płatności.", "failedToCompletePaymentSession": "Nie udało się zakończyć sesji płatności", - "noOrder": "Brak zamówienia" + "noOrder": "Brak zamówienia", + "confirmingPayment": "Potwierdzanie płatności..." }, "address": { "firstName": "Imię", @@ -248,7 +253,8 @@ "deleteConfirm": "Czy na pewno chcesz usunąć ten adres?", "setAsDefault": "Ustaw jako domyślny", "showPassword": "Pokaż hasło", - "hidePassword": "Ukryj hasło" + "hidePassword": "Ukryj hasło", + "forgotPassword": "Zapomniałeś hasła?" }, "register": { "createAccount": "Utwórz konto", @@ -311,7 +317,8 @@ "notAvailable": "Brak", "unknownShipmentStatus": "Nieznany", "orderTitle": "Zamówienie #{number}", - "shipmentCanceledRefund": "Przesyłka anulowana — zwrot został wystawiony." + "shipmentCanceledRefund": "Przesyłka anulowana — zwrot został wystawiony.", + "shippingMethodUnavailable": "Niedostępna" }, "orderPlaced": { "thanksForOrder": "Dziękujemy za zamówienie, {name}!", @@ -395,5 +402,37 @@ "expiresOn": "Wygasa {date}", "percentUsed": "{percent}% wykorzystano", "giftCardsHelpText": "Karty podarunkowe mogą być używane podczas składania zamówienia do opłacenia zakupów. Pozostałe saldo zostanie zapisane na przyszłe zakupy." + }, + "forgotPassword": { + "title": "Zresetuj hasło", + "description": "Podaj swój adres e-mail, a wyślemy Ci link do zresetowania hasła.", + "email": "E-mail", + "sending": "Wysyłanie...", + "sendResetLink": "Wyślij link resetujący", + "backToSignIn": "Wróć do logowania", + "checkYourEmail": "Sprawdź swoją skrzynkę", + "resetEmailSent": "Jeśli konto istnieje dla {email}, wysłaliśmy instrukcje resetowania hasła.", + "linkExpiry": "Link w wiadomości wygaśnie za 15 minut. Sprawdź folder spam, jeśli go nie widzisz.", + "tryDifferentEmail": "Spróbuj inny adres e-mail", + "genericError": "Coś poszło nie tak. Spróbuj ponownie." + }, + "resetPassword": { + "title": "Ustaw nowe hasło", + "description": "Wpisz poniżej nowe hasło.", + "newPassword": "Nowe hasło", + "confirmPassword": "Potwierdź nowe hasło", + "resetting": "Resetowanie...", + "resetPassword": "Zresetuj hasło", + "backToSignIn": "Wróć do logowania", + "invalidLink": "Nieprawidłowy link", + "invalidLinkDescription": "Ten link do resetowania hasła jest nieprawidłowy lub wygasł.", + "requestNewLink": "Poproś o nowy link resetujący", + "passwordsDontMatch": "Hasła nie są takie same", + "passwordTooShort": "Hasło musi mieć co najmniej 6 znaków", + "linkExpired": "Ten link resetujący wygasł. Poproś o nowy.", + "genericError": "Coś poszło nie tak. Spróbuj ponownie.", + "success": "Hasło zostało zresetowane", + "successDescription": "Twoje hasło zostało zaktualizowane. Możesz się teraz zalogować nowym hasłem.", + "signIn": "Zaloguj się" } } diff --git a/src/app/[country]/[locale]/(checkout)/confirm-payment/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/confirm-payment/[id]/page.tsx index 5587d679..ad89b41d 100644 --- a/src/app/[country]/[locale]/(checkout)/confirm-payment/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/confirm-payment/[id]/page.tsx @@ -2,6 +2,7 @@ import { Loader2 } from "lucide-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { use, useEffect, useRef } from "react"; import { confirmPaymentAndCompleteCart } from "@/lib/data/payment"; import { extractBasePath } from "@/lib/utils/path"; @@ -27,6 +28,7 @@ export default function ConfirmPaymentPage({ params, }: ConfirmPaymentPageProps) { const { id: cartId } = use(params); + const t = useTranslations("checkout"); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -57,7 +59,7 @@ export default function ConfirmPaymentPage({ router.replace(`${basePath}/order-placed/${cartId}`); } else { const errorMessage = encodeURIComponent( - result.error || "Payment could not be confirmed. Please try again.", + result.error || t("paymentError"), ); router.replace( `${basePath}/checkout/${cartId}?payment_error=${errorMessage}`, @@ -66,12 +68,12 @@ export default function ConfirmPaymentPage({ } confirmAndRedirect(); - }, [cartId, searchParams, basePath, router]); + }, [cartId, searchParams, basePath, router, t]); return (
    -

    Confirming your payment...

    +

    {t("confirmingPayment")}

    ); } diff --git a/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx b/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx index 0a601d04..79ff9a36 100644 --- a/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx @@ -3,6 +3,7 @@ import { CircleAlert, CircleCheck, Mail } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -20,6 +21,7 @@ import { requestPasswordReset } from "@/lib/data/customer"; import { extractBasePath } from "@/lib/utils/path"; export default function ForgotPasswordPage() { + const t = useTranslations("forgotPassword"); const pathname = usePathname(); const basePath = extractBasePath(pathname); @@ -40,10 +42,10 @@ export default function ForgotPasswordPage() { if (result?.message) { setSubmitted(true); } else { - setError("Something went wrong. Please try again."); + setError(t("genericError")); } } catch { - setError("Something went wrong. Please try again."); + setError(t("genericError")); } finally { setSubmitting(false); } @@ -57,20 +59,19 @@ export default function ForgotPasswordPage() {
    - Check your email + {t("checkYourEmail")} - If an account exists for {email}, we've sent - password reset instructions. + {t.rich("resetEmailSent", { + email, + strong: (chunks) => {chunks}, + })}
    -

    - The link in the email will expire in 15 minutes. Check your spam - folder if you don't see it. -

    +

    {t("linkExpiry")}

    @@ -90,7 +91,7 @@ export default function ForgotPasswordPage() { href={`${basePath}/account`} className="text-sm text-primary hover:text-primary/70 font-medium" > - Back to sign in + {t("backToSignIn")} @@ -102,11 +103,8 @@ export default function ForgotPasswordPage() {
    - Reset your password - - Enter your email and we'll send you a link to reset your - password. - + {t("title")} + {t("description")} @@ -119,7 +117,7 @@ export default function ForgotPasswordPage() { )} - Email + {t("email")} - {submitting ? "Sending..." : "Send reset link"} + {submitting ? t("sending") : t("sendResetLink")}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index 0f1364dd..82df64ad 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -19,6 +19,10 @@ import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; import { getOrder } from "@/lib/data/orders"; import { getCardIconType, getCardLabel } from "@/lib/utils/credit-card"; +import { + getShipmentStatusColor, + SHIPMENT_STATE_KEY, +} from "@/lib/utils/order-status"; import { extractBasePath } from "@/lib/utils/path"; function formatDate(dateString: string | null, locale: string): string { @@ -32,21 +36,6 @@ function formatDate(dateString: string | null, locale: string): string { }); } -function getShipmentStatusColor(state: string): string { - switch (state) { - case "shipped": - case "delivered": - return "bg-green-100 text-green-800"; - case "ready": - case "pending": - return "bg-yellow-100 text-yellow-800"; - case "canceled": - return "bg-red-100 text-red-800"; - default: - return "bg-gray-100 text-gray-800"; - } -} - function PaymentSourceInfo({ payment }: { payment: Payment }) { const t = useTranslations("orders"); const source = payment.source; @@ -202,7 +191,8 @@ function ShipmentBlock({ {t("shippingMethod")}

    - {shipment.shipping_method?.name || t("canceled")} + {shipment.shipping_method?.name || + t("shippingMethodUnavailable")}

    {shipment.stock_location && (

    @@ -212,7 +202,9 @@ function ShipmentBlock({ - {shipment.state} + {t( + SHIPMENT_STATE_KEY[shipment.state] || "unknownShipmentStatus", + )}

    @@ -228,7 +220,7 @@ function ShipmentBlock({ ) : ( )}
    @@ -238,16 +230,14 @@ function ShipmentBlock({ {shipment.state === "canceled" && !shipment.shipped_at && ( - - {t("shipmentCanceled")} — {t("refundIssued")} - + {t("shipmentCanceledRefund")} )} {shipment.state !== "canceled" && shipment.state !== "shipped" && !shipment.tracking && (
    - {t("noTracking")} + {t("noTrackingInfo")}
    )}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/page.tsx b/src/app/[country]/[locale]/(storefront)/account/page.tsx index 3adec0f5..539605f6 100644 --- a/src/app/[country]/[locale]/(storefront)/account/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/page.tsx @@ -57,7 +57,7 @@ export default function AccountPage() { router.push(redirectUrl); } } else { - setError(result.error || "Invalid email or password"); + setError(result.error || t("invalidCredentials")); } setLoading(false); }; @@ -147,7 +147,7 @@ export default function AccountPage() { href={`${basePath}/account/forgot-password`} className="text-sm text-primary hover:text-primary/70 font-medium" > - Forgot password? + {t("forgotPassword")}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx index b2cae104..318b19bb 100644 --- a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx @@ -55,12 +55,12 @@ export default function RegisterPage() { setError(null); if (password !== passwordConfirmation) { - setError("Passwords do not match"); + setError(t("passwordsDontMatch")); return; } if (password.length < 6) { - setError("Password must be at least 6 characters"); + setError(t("passwordTooShort")); return; } @@ -77,10 +77,10 @@ export default function RegisterPage() { if (result.success) { router.push(`${basePath}/account`); } else { - setError(result.error || "Registration failed. Please try again."); + setError(result.error || t("registrationFailed")); } } catch { - setError("An unexpected error occurred. Please try again."); + setError(t("unexpectedError")); } finally { setSubmitting(false); } diff --git a/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx b/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx index 2197096b..fbc558d8 100644 --- a/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx @@ -3,6 +3,7 @@ import { CircleAlert, CircleCheck, Eye, EyeOff } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -20,6 +21,8 @@ import { resetPassword } from "@/lib/data/customer"; import { extractBasePath } from "@/lib/utils/path"; export default function ResetPasswordPage() { + const t = useTranslations("resetPassword"); + const ta = useTranslations("account"); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -41,10 +44,8 @@ export default function ResetPasswordPage() {
    - Invalid link - - This password reset link is invalid or has expired. - + {t("invalidLink")} + {t("invalidLinkDescription")} @@ -52,7 +53,7 @@ export default function ResetPasswordPage() { href={`${basePath}/account/forgot-password`} className="text-sm text-primary hover:text-primary/70 font-medium" > - Request a new reset link + {t("requestNewLink")} @@ -65,12 +66,12 @@ export default function ResetPasswordPage() { setError(null); if (password !== passwordConfirmation) { - setError("Passwords do not match"); + setError(t("passwordsDontMatch")); return; } if (password.length < 6) { - setError("Password must be at least 6 characters"); + setError(t("passwordTooShort")); return; } @@ -81,13 +82,10 @@ export default function ResetPasswordPage() { if (result.success) { setSuccess(true); } else { - setError( - result.error || - "This reset link has expired. Please request a new one.", - ); + setError(result.error || t("linkExpired")); } } catch { - setError("Something went wrong. Please try again."); + setError(t("genericError")); } finally { setSubmitting(false); } @@ -101,11 +99,8 @@ export default function ResetPasswordPage() {
    - Password reset successful - - Your password has been updated. You can now sign in with your new - password. - + {t("success")} + {t("successDescription")} @@ -114,7 +109,7 @@ export default function ResetPasswordPage() { className="w-full" onClick={() => router.push(`${basePath}/account`)} > - Sign in + {t("signIn")} @@ -126,8 +121,8 @@ export default function ResetPasswordPage() {
    - Set a new password - Enter your new password below. + {t("title")} + {t("description")} @@ -140,7 +135,7 @@ export default function ResetPasswordPage() { )} - New password + {t("newPassword")}
    setShowPassword(!showPassword)} aria-label={ - showPassword ? "Hide password" : "Show password" + showPassword ? ta("hidePassword") : ta("showPassword") } > {showPassword ? ( @@ -175,7 +170,7 @@ export default function ResetPasswordPage() { - Confirm new password + {t("confirmPassword")}
    {showPasswordConfirmation ? ( @@ -220,7 +215,7 @@ export default function ResetPasswordPage() { size="lg" className="w-full" > - {submitting ? "Resetting..." : "Reset password"} + {submitting ? t("resetting") : t("resetPassword")}
    @@ -231,7 +226,7 @@ export default function ResetPasswordPage() { href={`${basePath}/account`} className="text-sm text-primary hover:text-primary/70 font-medium" > - Back to sign in + {t("backToSignIn")} diff --git a/src/components/checkout/AddressSection.tsx b/src/components/checkout/AddressSection.tsx index 27f25b7f..a4438953 100644 --- a/src/components/checkout/AddressSection.tsx +++ b/src/components/checkout/AddressSection.tsx @@ -3,6 +3,7 @@ import type { Address, AddressParams, Cart, Country, State } from "@spree/sdk"; import { Loader2 } from "lucide-react"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useRef, useState } from "react"; import { AddressEditModal } from "@/components/checkout/AddressEditModal"; import { AddressFormFields } from "@/components/checkout/AddressFormFields"; @@ -75,6 +76,10 @@ export function AddressSection({ saving, processing, }: AddressSectionProps) { + const t = useTranslations("checkout"); + const tc = useTranslations("common"); + const ta = useTranslations("address"); + // Determine initial saved address: use the first saved address when the // cart doesn't have a shipping address yet (authenticated users). const initialSavedAddress = @@ -229,13 +234,15 @@ export function AddressSection({ {/* Contact section */}
    -

    Contact

    +

    + {t("contactInformation")} +

    {!isAuthenticated && ( - Sign in + {t("signIn")} )}
    @@ -247,11 +254,11 @@ export function AddressSection({ onChange={(e) => setEmail(e.target.value)} onBlur={handleEmailBlur} disabled={isAuthenticated} - placeholder="Email" + placeholder={t("emailAddress")} /> {isAuthenticated && (

    - Using your account email address + {t("usingAccountEmail")}

    )}
    @@ -259,11 +266,13 @@ export function AddressSection({ {/* Delivery section */}
    -

    Delivery

    +

    + {t("shippingAddress")} +

    {saving && ( - Saving + {tc("saving")} )}
    @@ -306,7 +315,7 @@ export function AddressSection({ fetchStates={fetchStates} onSave={handleSaveEditedAddress} onClose={() => setEditingAddress(null)} - title="Edit Address" + title={ta("editAddress")} /> )} diff --git a/src/components/checkout/PaymentSection.tsx b/src/components/checkout/PaymentSection.tsx index 3195c32a..d64f66ca 100644 --- a/src/components/checkout/PaymentSection.tsx +++ b/src/components/checkout/PaymentSection.tsx @@ -8,6 +8,7 @@ import type { State, } from "@spree/sdk"; import { CircleAlert, CreditCard, Loader2 } from "lucide-react"; +import { useTranslations } from "next-intl"; import { forwardRef, useCallback, @@ -74,6 +75,8 @@ export const PaymentSection = forwardRef< }, ref, ) { + const t = useTranslations("checkout"); + // Initialize billing address from cart, check if it matches shipping const shipAddressData = useMemo( () => addressToFormData(cart.ship_address), @@ -146,21 +149,21 @@ export const PaymentSection = forwardRef< setClientSecret(secret); setPaymentSessionId(result.session.id); } else { - setGatewayError("Failed to initialize payment. Please try again."); + setGatewayError(t("failedToInitPayment")); } } else if (!result.success) { - setGatewayError(result.error || "Failed to create payment session."); + setGatewayError(result.error || t("failedToCreateSession")); } } catch { if (requestId !== sessionRequestIdRef.current) return; - setGatewayError("Failed to initialize payment. Please try again."); + setGatewayError(t("failedToInitPayment")); } finally { if (requestId === sessionRequestIdRef.current) { setLoading(false); } } }, - [sessionPaymentMethod, cart.id], + [sessionPaymentMethod, cart.id, t], ); // Track the cart total so we can recreate the session when it changes @@ -247,10 +250,10 @@ export const PaymentSection = forwardRef< () => ({ submit: async () => { if (!paymentSessionId || !clientSecret) { - return { error: "Payment not ready" }; + return { error: t("failedToInitPayment") }; } if (!selectedCardId && !gatewayHandleRef.current) { - return { error: "Payment not ready" }; + return { error: t("failedToInitPayment") }; } setProcessing(true); @@ -267,7 +270,7 @@ export const PaymentSection = forwardRef< if (!addressSuccess) { setProcessing(false); - return { error: "Failed to save billing address" }; + return { error: t("failedToSaveBilling") }; } // 2. Confirm payment with gateway @@ -302,7 +305,7 @@ export const PaymentSection = forwardRef< await onPaymentComplete(paymentSessionId); return {}; } catch { - const msg = "An error occurred during payment. Please try again."; + const msg = t("paymentError"); setGatewayError(msg); setProcessing(false); return { error: msg }; @@ -320,6 +323,7 @@ export const PaymentSection = forwardRef< onPaymentComplete, cart.id, setProcessing, + t, ], ); @@ -328,9 +332,9 @@ export const PaymentSection = forwardRef< return (
    {/* Section Header */} -

    Payment

    +

    {t("paymentMethod")}

    - All transactions are secure and encrypted. + {t("secureTransactions")}

    {/* Inline requirement errors from parent */} @@ -373,14 +377,15 @@ export const PaymentSection = forwardRef< width={34} /> - {getCardLabel(card.cc_type)} ending in {card.last_digits} + {getCardLabel(card.cc_type)} {t("endingIn")}{" "} + {card.last_digits} - Exp {String(card.month).padStart(2, "0")}/{card.year} + {t("exp")} {String(card.month).padStart(2, "0")}/{card.year} {card.default && ( - Default + {t("default")} )} @@ -395,7 +400,7 @@ export const PaymentSection = forwardRef< - Add new payment method + {t("addNewPaymentMethod")} @@ -407,7 +412,7 @@ export const PaymentSection = forwardRef<
    - Credit card + {t("creditCard")}
    @@ -419,7 +424,7 @@ export const PaymentSection = forwardRef<
    - Loading payment form... + {t("loadingPaymentForm")}
    )} @@ -451,9 +456,7 @@ export const PaymentSection = forwardRef< className="w-10 h-10 text-gray-300 mx-auto mb-3" strokeWidth={1.5} /> -

    - No payment methods available for this cart. -

    +

    {t("noPaymentMethods")}

    )}
    @@ -468,9 +471,7 @@ export const PaymentSection = forwardRef< handleUseShippingChange(checked === true) } /> - - Use shipping address as billing address - + {t("sameAsShipping")} {!useShippingForBilling && ( diff --git a/src/components/checkout/ShippingMethodSection.tsx b/src/components/checkout/ShippingMethodSection.tsx index c7c8397c..a00af1c0 100644 --- a/src/components/checkout/ShippingMethodSection.tsx +++ b/src/components/checkout/ShippingMethodSection.tsx @@ -1,6 +1,7 @@ "use client"; import type { Shipment } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; interface ShippingMethodSectionProps { @@ -16,9 +17,13 @@ export function ShippingMethodSection({ processing, errors, }: ShippingMethodSectionProps) { + const t = useTranslations("checkout"); + return (
    -

    Shipping method

    +

    + {t("shippingMethod")} +

    {errors && errors.length > 0 && (
    @@ -32,7 +37,7 @@ export function ShippingMethodSection({ {shipments.length === 0 ? (
    - Enter your shipping address to view available shipping methods. + {t("enterShippingAddressForMethods")}
    ) : (
    @@ -44,11 +49,17 @@ export function ShippingMethodSection({
    {shipments.length > 1 && (

    - Shipment {index + 1} of {shipments.length} + {t("shipmentOf", { + current: index + 1, + total: shipments.length, + })} {shipment.stock_location?.name && ( {" "} - — Ships from {shipment.stock_location.name} + —{" "} + {t("shipsFrom", { + location: shipment.stock_location.name, + })} )}

    diff --git a/src/components/checkout/Summary.tsx b/src/components/checkout/Summary.tsx index 92d47cf0..c0e20ea8 100644 --- a/src/components/checkout/Summary.tsx +++ b/src/components/checkout/Summary.tsx @@ -1,6 +1,7 @@ "use client"; import type { Cart } from "@spree/sdk"; +import { useTranslations } from "next-intl"; import { ProductImage } from "@/components/ui/product-image"; interface SummaryProps { @@ -8,6 +9,8 @@ interface SummaryProps { } export function Summary({ cart }: SummaryProps) { + const tc = useTranslations("common"); + const t = useTranslations("checkout"); const items = cart.items || []; const hasShipping = (cart.shipments?.length ?? 0) > 0; @@ -50,38 +53,40 @@ export function Summary({ cart }: SummaryProps) { {/* Totals — Shopify style */}
    - Subtotal + {tc("subtotal")} {cart.display_item_total}
    - Shipping + {tc("shipping")} {hasShipping ? ( {cart.display_ship_total} ) : ( - Enter shipping address + {t("enterShippingAddress")} )}
    {parseFloat(cart.promo_total) !== 0 && (
    - Discount + {tc("discount")} {cart.display_promo_total}
    )} {parseFloat(cart.tax_total) > 0 && (
    - Tax + {tc("tax")} {cart.display_tax_total}
    )} {/* Total row — Shopify: bold, larger, with currency code */}
    - Total + + {tc("total")} +
    {cart.currency} From 61ba21cc1774bf2d80e2ab6dfc57b90492db6d75 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Mon, 16 Mar 2026 11:20:07 +0100 Subject: [PATCH 08/28] Fix confirm-payment test: add next-intl mock for useTranslations The test was missing a next-intl mock after i18n was added to the confirm-payment page. Uses the same identity-function mock pattern as ProductCard tests. Co-Authored-By: Claude Opus 4.6 --- .../(checkout)/confirm-payment/__tests__/page.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx b/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx index 5f95645a..9d6f1f1e 100644 --- a/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx +++ b/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx @@ -5,6 +5,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mockReplace = vi.fn(); let mockSearchParams = new URLSearchParams(); +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn(), @@ -56,7 +60,7 @@ describe("ConfirmPaymentPage", () => { renderPage(); }); - expect(screen.getByText("Confirming your payment...")).toBeInTheDocument(); + expect(screen.getByText("confirmingPayment")).toBeInTheDocument(); }); it("redirects to order-placed on success", async () => { @@ -123,9 +127,7 @@ describe("ConfirmPaymentPage", () => { await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith( - expect.stringContaining( - "payment_error=Payment%20could%20not%20be%20confirmed", - ), + expect.stringContaining("payment_error=paymentError"), ); }); }); From a754edb2c86ca1ae68230b93a22a857a18f7fc1f Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Mon, 16 Mar 2026 15:42:16 +0100 Subject: [PATCH 09/28] Fix infinite re-render loop on checkout page useRouter() and useTranslations() return unstable references on each render, causing useCallback/useEffect dependency chains to fire in a loop. Stabilize them with useRef (routerRef, tRef) so callbacks keep empty or minimal dependency arrays. Co-Authored-By: Claude Opus 4.6 --- .../(checkout)/checkout/[id]/page.tsx | 95 ++++++++++--------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx index 9ca19cef..f2d25277 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx @@ -102,37 +102,37 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const cartRef = useRef(cart); cartRef.current = cart; + const routerRef = useRef(router); + routerRef.current = router; + const tRef = useRef(t); + tRef.current = t; const beginCheckoutFiredRef = useRef(false); const paymentRef = useRef(null); // Handle coupon code application - const handleApplyCoupon = useCallback( - async (code: string) => { - const currentOrder = cartRef.current; - if (!currentOrder) return { success: false, error: t("noOrder") }; + const handleApplyCoupon = useCallback(async (code: string) => { + const currentOrder = cartRef.current; + if (!currentOrder) + return { success: false, error: tRef.current("noOrder") }; - const result = await applyCouponCode(currentOrder.id, code); - if (result.success && result.cart) { - setCart(result.cart); - } - return result; - }, - [t], - ); + const result = await applyCouponCode(currentOrder.id, code); + if (result.success && result.cart) { + setCart(result.cart); + } + return result; + }, []); - const handleRemoveCoupon = useCallback( - async (couponCode: string) => { - const currentOrder = cartRef.current; - if (!currentOrder) return { success: false, error: t("noOrder") }; + const handleRemoveCoupon = useCallback(async (couponCode: string) => { + const currentOrder = cartRef.current; + if (!currentOrder) + return { success: false, error: tRef.current("noOrder") }; - const result = await removeCouponCode(currentOrder.id, couponCode); - if (result.success && result.cart) { - setCart(result.cart); - } - return result; - }, - [t], - ); + const result = await removeCouponCode(currentOrder.id, couponCode); + if (result.success && result.cart) { + setCart(result.cart); + } + return result; + }, []); // Track cart key for sidebar updates const cartKey = cart ? `${cart.id}-${cart.updated_at}` : null; @@ -180,13 +180,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { : { data: [] as Country[] }; if (!cartData) { - setError(t("orderNotFound")); + setError(tRef.current("orderNotFound")); setLoading(false); return; } if (cartData.current_step === "complete") { - router.push(`${basePath}/order-placed/${cartId}`); + routerRef.current.push(`${basePath}/order-placed/${cartId}`); return; } @@ -206,12 +206,12 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { return cartData; } catch { - setError(t("failedToLoadCheckout")); + setError(tRef.current("failedToLoadCheckout")); return null; } finally { setLoading(false); } - }, [cartId, urlCountry, basePath, router, paymentError, t]); + }, [cartId, urlCountry, basePath, paymentError]); useEffect(() => { loadOrder(); @@ -260,7 +260,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { }); if (!updateResult.success) { - setError(updateResult.error || t("failedToSaveAddress")); + setError(updateResult.error || tRef.current("failedToSaveAddress")); return; } @@ -268,12 +268,12 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { setCart(updateResult.cart); } } catch { - setError(t("generalError")); + setError(tRef.current("generalError")); } finally { setSaving(false); } }, - [t], + [], ); // Handle shipping rate selection @@ -295,7 +295,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { rateId, ); if (!result.success) { - setError(result.error || t("failedToSelectRate")); + setError(result.error || tRef.current("failedToSelectRate")); } else if (result.cart) { setCart(result.cart); @@ -306,7 +306,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { trackingRateName = selectedRate?.name; } } catch { - setError(t("generalError")); + setError(tRef.current("generalError")); } finally { setProcessing(false); } @@ -319,7 +319,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { } } }, - [t], + [], ); // Handle billing address update (called by PaymentSection before gateway confirmation) @@ -336,17 +336,17 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { }); if (!updateResult.success) { - setError(updateResult.error || t("failedToSaveBilling")); + setError(updateResult.error || tRef.current("failedToSaveBilling")); return false; } return true; } catch { - setError(t("failedToSaveBillingRetry")); + setError(tRef.current("failedToSaveBillingRetry")); return false; } }, - [t], + [], ); // Handle payment completion (called by PaymentSection after Stripe confirms) @@ -364,7 +364,10 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { ); if (!sessionResult.success) { - setError(sessionResult.error || t("failedToCompletePaymentSession")); + setError( + sessionResult.error || + tRef.current("failedToCompletePaymentSession"), + ); setProcessing(false); return; } @@ -379,7 +382,9 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // session completion, completeCheckoutOrder handles 403/422 gracefully. const completeResult = await completeCheckoutOrder(currentOrder.id); if (!completeResult.success) { - setError(completeResult.error || t("failedToCompleteOrder")); + setError( + completeResult.error || tRef.current("failedToCompleteOrder"), + ); setProcessing(false); return; } @@ -392,13 +397,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { cacheCompletedOrder(currentOrder.id, completeResult.order); } - router.push(`${basePath}/order-placed/${currentOrder.id}`); + routerRef.current.push(`${basePath}/order-placed/${currentOrder.id}`); } catch { - setError(t("generalError")); + setError(tRef.current("generalError")); setProcessing(false); } }, - [basePath, router, t], + [basePath], ); // Fetch states for a country @@ -417,16 +422,16 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const result = await updateAddress(id, data); if (!result.success) { - throw new Error(result.error || t("failedToSaveAddress")); + throw new Error(result.error || tRef.current("failedToSaveAddress")); } if (!result.address) { - throw new Error(t("generalError")); + throw new Error(tRef.current("generalError")); } return result.address; }, - [t], + [], ); // Validate and pay — single "Pay now" action From 9f188913624ef6cdf7f38db84ea77cc9f9572994 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Mon, 16 Mar 2026 18:16:07 +0100 Subject: [PATCH 10/28] Fix i18n issues: use interpolation keys, remove hardcoded strings, improve a11y - Replace embedded English "powered by" in de.json with "betrieben von" - Add tsx as devDependency and use local bin in check:locales script - Use vi.importActual in confirm-payment test mock; tighten fallback assertion - Remove CSS capitalize from order status badges (rely on translations) - Use raw state string as fallback instead of passing unmapped keys to t() - Move CartDrawer item count parentheses into translation strings - Replace concatenated saved-card strings with savedCardLabel/cardExpiry keys - Replace concatenated price chip label with priceLabel interpolation key - Add clearFilter key for accessible chip remove button aria-labels - Add partial (blue) and backorder (yellow) to getShipmentStatusColor - Replace hardcoded "Back to sign in" with t("backToSignIn") - Replace hardcoded "Open image zoom" aria-label with t("openImageZoom") - Add directory existence check to check-locale-parity script - Always include aria-controls on checkout summary toggle - Store error keys instead of translated strings in order-placed page - Use t("selectState") for state select prompt in AddressFormFields - Use nullish coalescing for emptyMessage fallback in ProductListingLayout - Use query.trim() in SearchBar viewAllResultsFor label - Add English fallback maps in filters.ts for when t() is unavailable - Update next-intl.d.ts to recommended AppConfig module augmentation Co-Authored-By: Claude Opus 4.6 --- messages/de.json | 11 +++-- messages/en.json | 7 ++- messages/pl.json | 7 ++- package-lock.json | 45 +++++++++++++++++++ package.json | 3 +- scripts/check-locale-parity.ts | 7 ++- .../confirm-payment/__tests__/page.test.tsx | 14 ++++-- .../[country]/[locale]/(checkout)/layout.tsx | 2 +- .../(checkout)/order-placed/[id]/page.tsx | 8 ++-- .../account/forgot-password/page.tsx | 2 +- .../(storefront)/account/orders/[id]/page.tsx | 2 +- .../(storefront)/account/orders/page.tsx | 18 ++++---- src/components/cart/CartDrawer.tsx | 4 +- src/components/checkout/AddressFormFields.tsx | 2 +- src/components/checkout/PaymentSection.tsx | 11 +++-- src/components/products/MediaGallery.tsx | 2 +- .../products/ProductListingLayout.tsx | 2 +- .../products/filters/FilterChips.tsx | 6 ++- src/components/search/SearchBar.tsx | 2 +- src/lib/utils/filters.ts | 20 ++++++++- src/lib/utils/order-status.ts | 3 ++ src/types/next-intl.d.ts | 6 ++- 22 files changed, 141 insertions(+), 43 deletions(-) diff --git a/messages/de.json b/messages/de.json index fb4453a1..04c0747a 100644 --- a/messages/de.json +++ b/messages/de.json @@ -26,7 +26,7 @@ "account": "Konto" }, "footer": { - "description": "Ein moderner E-Commerce-Shop powered by Spree Commerce und Next.js.", + "description": "Ein moderner E-Commerce-Shop, betrieben von Spree Commerce und Next.js.", "shop": "Shop", "allProducts": "Alle Produkte", "categories": "Kategorien", @@ -38,7 +38,7 @@ }, "home": { "welcome": "Willkommen bei {storeName}", - "heroDescription": "Entdecken Sie großartige Produkte mit unserem modernen E-Commerce-Erlebnis powered by Spree Commerce.", + "heroDescription": "Entdecken Sie großartige Produkte mit unserem modernen E-Commerce-Erlebnis, betrieben von Spree Commerce.", "shopNow": "Jetzt einkaufen", "browseCategories": "Kategorien durchsuchen", "featuredProducts": "Ausgewählte Produkte", @@ -63,7 +63,7 @@ "viewCart": "Warenkorb anzeigen", "shoppingCart": "Warenkorb", "proceedToCheckout": "Zur Kasse gehen", - "itemCount": "{count, plural, one {# Artikel} other {# Artikel}}", + "itemCount": "({count, plural, one {# Artikel} other {# Artikel}})", "shippingCalculatedAtCheckout": "Wird an der Kasse berechnet", "removeItemLabel": "{name} entfernen" }, @@ -132,6 +132,9 @@ "closeFilters": "Filter schließen", "anyAvailability": "Beliebige Verfügbarkeit", "customPrice": "Benutzerdefiniert", + "priceLabel": "Preis: {value}", + "clearFilter": "{label} löschen", + "openImageZoom": "Bildzoom öffnen", "priceUnder": "Unter {price}", "priceAbove": "{price}+", "priceRangeBucket": "{min} – {max}" @@ -173,6 +176,8 @@ "addNewPaymentMethod": "Neue Zahlungsmethode hinzufügen", "endingIn": "endet auf", "exp": "Gültig bis", + "savedCardLabel": "{brand} endet auf {digits}", + "cardExpiry": "Gültig bis {month}/{year}", "default": "Standard", "noPaymentMethods": "Für diese Bestellung sind keine Zahlungsmethoden verfügbar.", "secureTransactions": "Alle Transaktionen sind sicher und verschlüsselt.", diff --git a/messages/en.json b/messages/en.json index 6df82b92..3cdf374c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -63,7 +63,7 @@ "viewCart": "View Cart", "shoppingCart": "Shopping Cart", "proceedToCheckout": "Proceed to Checkout", - "itemCount": "{count, plural, one {# item} other {# items}}", + "itemCount": "({count, plural, one {# item} other {# items}})", "shippingCalculatedAtCheckout": "Calculated at checkout", "removeItemLabel": "Remove {name}" }, @@ -132,6 +132,9 @@ "closeFilters": "Close filters", "anyAvailability": "Any availability", "customPrice": "Custom", + "priceLabel": "Price: {value}", + "clearFilter": "Clear {label}", + "openImageZoom": "Open image zoom", "priceUnder": "Under {price}", "priceAbove": "{price}+", "priceRangeBucket": "{min} – {max}" @@ -173,6 +176,8 @@ "addNewPaymentMethod": "Add new payment method", "endingIn": "ending in", "exp": "Exp", + "savedCardLabel": "{brand} ending in {digits}", + "cardExpiry": "Exp {month}/{year}", "default": "Default", "noPaymentMethods": "No payment methods available for this order.", "secureTransactions": "All transactions are secure and encrypted.", diff --git a/messages/pl.json b/messages/pl.json index ac005fe6..e9aff216 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -63,7 +63,7 @@ "viewCart": "Zobacz koszyk", "shoppingCart": "Koszyk zakupowy", "proceedToCheckout": "Przejdź do kasy", - "itemCount": "{count, plural, one {# produkt} few {# produkty} many {# produktów} other {# produktów}}", + "itemCount": "({count, plural, one {# produkt} few {# produkty} many {# produktów} other {# produktów}})", "shippingCalculatedAtCheckout": "Obliczona przy kasie", "removeItemLabel": "Usuń {name}" }, @@ -132,6 +132,9 @@ "closeFilters": "Zamknij filtry", "anyAvailability": "Dowolna dostępność", "customPrice": "Niestandardowa", + "priceLabel": "Cena: {value}", + "clearFilter": "Wyczyść {label}", + "openImageZoom": "Otwórz powiększenie zdjęcia", "priceUnder": "Poniżej {price}", "priceAbove": "{price}+", "priceRangeBucket": "{min} – {max}" @@ -173,6 +176,8 @@ "addNewPaymentMethod": "Dodaj nową metodę płatności", "endingIn": "kończąca się na", "exp": "Ważna do", + "savedCardLabel": "{brand} kończąca się na {digits}", + "cardExpiry": "Ważna do {month}/{year}", "default": "Domyślna", "noPaymentMethods": "Brak dostępnych metod płatności dla tego zamówienia.", "secureTransactions": "Wszystkie transakcje są bezpieczne i szyfrowane.", diff --git a/package-lock.json b/package-lock.json index 09c81116..67616caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "jsdom": "^28.0.0", "lefthook": "^2.1.2", "tailwindcss": "^4", + "tsx": "^4.19.0", "typescript": "^5", "vitest": "^4.0.18" } @@ -8705,6 +8706,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -11390,6 +11404,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -12452,6 +12476,27 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", diff --git a/package.json b/package.json index a33f7d3c..10023bc8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "check": "biome check .", "test": "vitest run", "test:watch": "vitest", - "check:locales": "npx tsx scripts/check-locale-parity.ts" + "check:locales": "tsx scripts/check-locale-parity.ts" }, "dependencies": { "@next/third-parties": "^16.1.6", @@ -54,6 +54,7 @@ "lefthook": "^2.1.2", "tailwindcss": "^4", "typescript": "^5", + "tsx": "^4.19.0", "vitest": "^4.0.18" } } diff --git a/scripts/check-locale-parity.ts b/scripts/check-locale-parity.ts index a69387c0..ef43f34b 100644 --- a/scripts/check-locale-parity.ts +++ b/scripts/check-locale-parity.ts @@ -1,6 +1,6 @@ /** * Checks that all locale JSON files have the same set of keys as en.json. - * Run: npx tsx scripts/check-locale-parity.ts + * Run: tsx scripts/check-locale-parity.ts */ import fs from "node:fs"; import path from "node:path"; @@ -8,6 +8,11 @@ import path from "node:path"; const MESSAGES_DIR = path.resolve(__dirname, "../messages"); const BASE_LOCALE = "en"; +if (!fs.existsSync(MESSAGES_DIR) || !fs.statSync(MESSAGES_DIR).isDirectory()) { + console.error(`Messages directory not found: ${MESSAGES_DIR}`); + process.exit(1); +} + function getNestedKeys(obj: Record, prefix = ""): string[] { const keys: string[] = []; for (const key of Object.keys(obj)) { diff --git a/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx b/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx index 9d6f1f1e..68134e81 100644 --- a/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx +++ b/src/app/[country]/[locale]/(checkout)/confirm-payment/__tests__/page.test.tsx @@ -5,9 +5,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mockReplace = vi.fn(); let mockSearchParams = new URLSearchParams(); -vi.mock("next-intl", () => ({ - useTranslations: () => (key: string) => key, -})); +vi.mock("next-intl", async () => { + const actual = await vi.importActual("next-intl"); + return { + ...actual, + useTranslations: () => (key: string) => key, + }; +}); vi.mock("next/navigation", () => ({ useRouter: () => ({ @@ -127,7 +131,9 @@ describe("ConfirmPaymentPage", () => { await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith( - expect.stringContaining("payment_error=paymentError"), + expect.stringContaining( + "/us/en/checkout/cart-1?payment_error=paymentError", + ), ); }); }); diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index 1760c734..17a4cb94 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -63,7 +63,7 @@ function MobileSummaryToggle() { onClick={() => setIsOpen(!isOpen)} className="w-full px-5 py-4 flex items-center justify-between text-left" aria-expanded={isOpen} - {...(isOpen && { "aria-controls": "checkout-summary-panel" })} + aria-controls="checkout-summary-panel" > diff --git a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx index 26341553..9e27c27d 100644 --- a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx @@ -65,13 +65,13 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { // Analytics failure must not break the order confirmation UX } } else { - setError(t("orderNotFound")); + setError("orderNotFound"); } setLoading(false); } catch { if (!cancelled) { loadedRef.current = true; - setError(t("failedToLoad")); + setError("failedToLoad"); setLoading(false); } } @@ -82,7 +82,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { return () => { cancelled = true; }; - }, [cartId, t]); + }, [cartId]); if (loading) { return ( @@ -99,7 +99,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { return (

    - {error || t("orderNotFound")} + {t(error || "orderNotFound")}

    diff --git a/src/components/checkout/AddressFormFields.tsx b/src/components/checkout/AddressFormFields.tsx index ba2a3d12..66c5a8fc 100644 --- a/src/components/checkout/AddressFormFields.tsx +++ b/src/components/checkout/AddressFormFields.tsx @@ -135,7 +135,7 @@ export function AddressFormFields({ required > - {t("stateProvince")} + {t("selectState")} {states.map((state) => ( diff --git a/src/components/checkout/PaymentSection.tsx b/src/components/checkout/PaymentSection.tsx index d64f66ca..b53de399 100644 --- a/src/components/checkout/PaymentSection.tsx +++ b/src/components/checkout/PaymentSection.tsx @@ -377,11 +377,16 @@ export const PaymentSection = forwardRef< width={34} /> - {getCardLabel(card.cc_type)} {t("endingIn")}{" "} - {card.last_digits} + {t("savedCardLabel", { + brand: getCardLabel(card.cc_type), + digits: card.last_digits, + })} - {t("exp")} {String(card.month).padStart(2, "0")}/{card.year} + {t("cardExpiry", { + month: String(card.month).padStart(2, "0"), + year: String(card.year), + })} {card.default && ( diff --git a/src/components/products/MediaGallery.tsx b/src/components/products/MediaGallery.tsx index 5077076b..5d534d7f 100644 --- a/src/components/products/MediaGallery.tsx +++ b/src/components/products/MediaGallery.tsx @@ -54,7 +54,7 @@ export function MediaGallery({ images, productName }: MediaGalleryProps) { type="button" className="relative aspect-square bg-gray-100 rounded-xl overflow-hidden cursor-zoom-in w-full" onClick={() => showMainImage && setIsZoomed(true)} - aria-label="Open image zoom" + aria-label={t("openImageZoom")} disabled={!showMainImage} >

    - {emptyMessage || t("tryAdjustingFilters")} + {emptyMessage ?? t("tryAdjustingFilters")}

    ) : ( diff --git a/src/components/products/filters/FilterChips.tsx b/src/components/products/filters/FilterChips.tsx index 2cd393d9..f836bf85 100644 --- a/src/components/products/filters/FilterChips.tsx +++ b/src/components/products/filters/FilterChips.tsx @@ -65,7 +65,9 @@ export function FilterChips({ ); chips.push({ key: "price", - label: `${t("price")}: ${matchingBucket?.label || t("customPrice")}`, + label: t("priceLabel", { + value: matchingBucket?.label || t("customPrice"), + }), onRemove: onRemovePrice, }); } @@ -91,7 +93,7 @@ export function FilterChips({ diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 0e7b790d..e5a301ab 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -238,7 +238,7 @@ export function SearchBar({ basePath }: SearchBarProps) { }} className="w-full p-3 text-sm text-primary hover:bg-gray-50 text-center font-medium" > - {t("viewAllResultsFor", { query })} + {t("viewAllResultsFor", { query: query.trim() })} )} diff --git a/src/lib/utils/filters.ts b/src/lib/utils/filters.ts index 7f265252..286e6d03 100644 --- a/src/lib/utils/filters.ts +++ b/src/lib/utils/filters.ts @@ -49,11 +49,22 @@ export function normalizeSortKey(key: string): string { * Pass a `t` function from `useTranslations("products")`. * Falls back to the raw key if no translator is provided. */ +const SORT_FALLBACK: Record = { + manual: "Manual", + best_selling: "Best Selling", + price: "Price (low-high)", + "-price": "Price (high-low)", + "-available_on": "Newest", + available_on: "Oldest", + name: "Name (A-Z)", + "-name": "Name (Z-A)", +}; + export function getSortLabel(key: string, t?: (key: string) => string): string { const normalized = normalizeSortKey(key); const messageKey = SORT_KEY_TO_MESSAGE[normalized]; if (messageKey && t) return t(messageKey); - return key; + return SORT_FALLBACK[normalized] || key; } /** Maps availability API values to translation message keys in the "products" namespace. */ @@ -66,11 +77,16 @@ const AVAILABILITY_KEY_TO_MESSAGE: Record = { * Get a translated availability label. * Pass a `t` function from `useTranslations("products")`. */ +const AVAILABILITY_FALLBACK: Record = { + in_stock: "In Stock", + out_of_stock: "Out of Stock", +}; + export function getAvailabilityLabel( id: string, t?: (key: string) => string, ): string { const messageKey = AVAILABILITY_KEY_TO_MESSAGE[id]; if (messageKey && t) return t(messageKey); - return id; + return AVAILABILITY_FALLBACK[id] || id; } diff --git a/src/lib/utils/order-status.ts b/src/lib/utils/order-status.ts index 2958c297..7a0f6507 100644 --- a/src/lib/utils/order-status.ts +++ b/src/lib/utils/order-status.ts @@ -44,7 +44,10 @@ export function getShipmentStatusColor(state: string | null): string { return "bg-green-100 text-green-800"; case "ready": case "pending": + case "backorder": return "bg-yellow-100 text-yellow-800"; + case "partial": + return "bg-blue-100 text-blue-800"; case "canceled": return "bg-red-100 text-red-800"; default: diff --git a/src/types/next-intl.d.ts b/src/types/next-intl.d.ts index 9490a9ea..8b639cd9 100644 --- a/src/types/next-intl.d.ts +++ b/src/types/next-intl.d.ts @@ -2,6 +2,8 @@ import type messages from "../../messages/en.json"; type Messages = typeof messages; -declare global { - interface IntlMessages extends Messages {} +declare module "next-intl" { + interface AppConfig { + Messages: Messages; + } } From 0c1d738f20b54832a723e8858245df13794b7661 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Mon, 16 Mar 2026 18:22:37 +0100 Subject: [PATCH 11/28] Fix TypeScript errors: type-safe translation keys and revert next-intl.d.ts - Revert next-intl.d.ts to global IntlMessages pattern (AppConfig caused cascading type errors across all translator call sites) - Type order-placed error state as union of known error keys - Cast dynamic SHIPMENT_STATE_KEY/PAYMENT_STATE_KEY lookups via keyof IntlMessages["orders"] for type safety - Fix pre-existing bug: t("orderNumber") -> t("orderTitle") in order detail - Widen translator param type in filters.ts and price-buckets.ts to accept next-intl's Translator type (key: any instead of key: string) Co-Authored-By: Claude Opus 4.6 --- .../[locale]/(checkout)/order-placed/[id]/page.tsx | 4 +++- .../(storefront)/account/orders/[id]/page.tsx | 5 +++-- .../[locale]/(storefront)/account/orders/page.tsx | 12 ++++++++++-- src/lib/utils/filters.ts | 4 ++-- src/lib/utils/price-buckets.ts | 2 +- src/types/next-intl.d.ts | 6 ++---- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx index 9e27c27d..e13f4ab2 100644 --- a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx @@ -32,7 +32,9 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { const [order, setOrder] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState<"orderNotFound" | "failedToLoad" | null>( + null, + ); // Clear sidebar summary useEffect(() => { diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index a7abc6fc..80de6f76 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -203,7 +203,8 @@ function ShipmentBlock({ className={`inline-flex items-center mt-2 px-2.5 py-0.5 rounded-lg text-xs font-medium ${getShipmentStatusColor(shipment.state)}`} > {t( - SHIPMENT_STATE_KEY[shipment.state] || "unknownShipmentStatus", + (SHIPMENT_STATE_KEY[shipment.state] || + "unknownShipmentStatus") as keyof IntlMessages["orders"], )}
    @@ -326,7 +327,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { {/* Title */}

    - {t("orderNumber", { number: order.number })} + {t("orderTitle", { number: order.number })}

    {t("placedOn", { date: formatDate(order.completed_at, locale) })} diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx index bb38f354..f32b4109 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -132,7 +132,11 @@ export default function OrdersPage() { > {order.payment_state ? PAYMENT_STATE_KEY[order.payment_state] - ? t(PAYMENT_STATE_KEY[order.payment_state]) + ? t( + PAYMENT_STATE_KEY[ + order.payment_state + ] as keyof IntlMessages["orders"], + ) : order.payment_state : t("notAvailable")} @@ -143,7 +147,11 @@ export default function OrdersPage() { > {order.shipment_state ? SHIPMENT_STATE_KEY[order.shipment_state] - ? t(SHIPMENT_STATE_KEY[order.shipment_state]) + ? t( + SHIPMENT_STATE_KEY[ + order.shipment_state + ] as keyof IntlMessages["orders"], + ) : order.shipment_state : t("notAvailable")} diff --git a/src/lib/utils/filters.ts b/src/lib/utils/filters.ts index 286e6d03..0096df06 100644 --- a/src/lib/utils/filters.ts +++ b/src/lib/utils/filters.ts @@ -60,7 +60,7 @@ const SORT_FALLBACK: Record = { "-name": "Name (Z-A)", }; -export function getSortLabel(key: string, t?: (key: string) => string): string { +export function getSortLabel(key: string, t?: (key: any) => string): string { const normalized = normalizeSortKey(key); const messageKey = SORT_KEY_TO_MESSAGE[normalized]; if (messageKey && t) return t(messageKey); @@ -84,7 +84,7 @@ const AVAILABILITY_FALLBACK: Record = { export function getAvailabilityLabel( id: string, - t?: (key: string) => string, + t?: (key: any) => string, ): string { const messageKey = AVAILABILITY_KEY_TO_MESSAGE[id]; if (messageKey && t) return t(messageKey); diff --git a/src/lib/utils/price-buckets.ts b/src/lib/utils/price-buckets.ts index 4ddb407f..3eb2f983 100644 --- a/src/lib/utils/price-buckets.ts +++ b/src/lib/utils/price-buckets.ts @@ -30,7 +30,7 @@ function formatCurrency( * to get labels like "Poniżej $50" instead of "Under $50". */ interface BucketLabelOptions { - t?: (key: string, values?: Record) => string; + t?: (key: any, values?: Record) => string; locale?: string; } diff --git a/src/types/next-intl.d.ts b/src/types/next-intl.d.ts index 8b639cd9..9490a9ea 100644 --- a/src/types/next-intl.d.ts +++ b/src/types/next-intl.d.ts @@ -2,8 +2,6 @@ import type messages from "../../messages/en.json"; type Messages = typeof messages; -declare module "next-intl" { - interface AppConfig { - Messages: Messages; - } +declare global { + interface IntlMessages extends Messages {} } From 019f6c26e2ae2b99c0744c8e6fc368baefc2f23f Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Mon, 16 Mar 2026 19:04:22 +0100 Subject: [PATCH 12/28] Fix type safety issues and minor bugs from PR review - Replace `any` with `string` in translation helper params (filters.ts, price-buckets.ts) - Add type="button" to FilterChips remove button to prevent form submission - Add try/catch/finally to order detail loadOrder to prevent stuck loading state - Fix __dirname to process.cwd() in check-locale-parity for ESM compatibility Co-Authored-By: Claude Opus 4.6 --- scripts/check-locale-parity.ts | 2 +- .../(storefront)/account/orders/[id]/page.tsx | 11 ++++++++--- src/components/products/filters/FilterChips.tsx | 1 + src/lib/utils/filters.ts | 4 ++-- src/lib/utils/price-buckets.ts | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/check-locale-parity.ts b/scripts/check-locale-parity.ts index ef43f34b..3c80850c 100644 --- a/scripts/check-locale-parity.ts +++ b/scripts/check-locale-parity.ts @@ -5,7 +5,7 @@ import fs from "node:fs"; import path from "node:path"; -const MESSAGES_DIR = path.resolve(__dirname, "../messages"); +const MESSAGES_DIR = path.resolve(process.cwd(), "messages"); const BASE_LOCALE = "en"; if (!fs.existsSync(MESSAGES_DIR) || !fs.statSync(MESSAGES_DIR).isDirectory()) { diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index 80de6f76..a6d1196b 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -275,9 +275,14 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { useEffect(() => { async function loadOrder() { - const orderData = await getOrder(id); - setOrder(orderData); - setLoading(false); + try { + const orderData = await getOrder(id); + setOrder(orderData); + } catch { + setOrder(null); + } finally { + setLoading(false); + } } loadOrder(); }, [id]); diff --git a/src/components/products/filters/FilterChips.tsx b/src/components/products/filters/FilterChips.tsx index f836bf85..338652bd 100644 --- a/src/components/products/filters/FilterChips.tsx +++ b/src/components/products/filters/FilterChips.tsx @@ -91,6 +91,7 @@ export function FilterChips({ > {chip.label}

    ); } + +export default function CheckoutPage({ params }: CheckoutPageProps) { + return ( + + +
    + } + > + + + ); +} From afd038392d28ca9a425e938380ab53ffdbd4b9ed Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 19:35:00 +0200 Subject: [PATCH 15/28] Fix build error: remove cookies() call from i18n request config The cookies() API is runtime data that blocks prerendering. Since the locale is always available from the [locale] route segment, the cookie fallback was unnecessary. Simplified to only use requestLocale with "en" as default. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/request.ts | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/i18n/request.ts b/src/i18n/request.ts index f4c57744..04b3f67d 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -1,32 +1,23 @@ -import { cookies } from "next/headers"; import type { Locale } from "next-intl"; import { getRequestConfig } from "next-intl/server"; const supportedLocales: Locale[] = ["en", "de", "pl"]; -export default getRequestConfig(async ({ requestLocale }) => { - // Prefer the route-derived locale (from the [locale] path segment), - // fall back to the spree_locale cookie, then default to "en". - let locale = (await requestLocale) as Locale | undefined; +function isValidLocale(value: string | undefined): value is Locale { + return !!value && supportedLocales.includes(value as Locale); +} - if (locale && !supportedLocales.includes(locale)) { - // Invalid locale from route — default to "en" directly - locale = "en"; - } else if (!locale) { - // No locale from route — try cookie, then default to "en" - const cookieStore = await cookies(); - locale = cookieStore.get("spree_locale")?.value as Locale | undefined; - if (!locale || !supportedLocales.includes(locale)) { - locale = "en"; - } - } +export default getRequestConfig(async ({ requestLocale }) => { + // Prefer the route-derived locale (from the [locale] path segment). + // Default to "en" if the route segment is invalid or missing. + const requested = (await requestLocale) as string | undefined; + const locale: Locale = isValidLocale(requested) ? requested : "en"; let messages: IntlMessages; try { messages = (await import(`../../messages/${locale}.json`)).default; } catch { // Fallback to English if locale file doesn't exist - locale = "en"; messages = (await import("../../messages/en.json")).default; } From a5137ef5f209cd9716637baf8e7d4e11cb86fddc Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 19:39:44 +0200 Subject: [PATCH 16/28] Move NextIntlClientProvider from root layout to [locale] layout Root layout was calling getLocale()/getMessages() which access runtime data (cookies/headers), blocking prerendering in Next.js 16. Fix: move NextIntlClientProvider to [country]/[locale]/layout.tsx where the locale is available from route params. Pass locale explicitly to getMessages() so the i18n request config uses the explicit locale param instead of awaiting requestLocale (runtime data). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/[country]/[locale]/layout.tsx | 27 ++++++++++++++++++--------- src/app/layout.tsx | 25 +++++++++---------------- src/i18n/request.ts | 22 ++++++++++++++-------- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/app/[country]/[locale]/layout.tsx b/src/app/[country]/[locale]/layout.tsx index 519095f2..540152a4 100644 --- a/src/app/[country]/[locale]/layout.tsx +++ b/src/app/[country]/[locale]/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; import { JsonLd } from "@/components/seo/JsonLd"; import { AuthProvider } from "@/contexts/AuthContext"; import { StoreProvider } from "@/contexts/StoreContext"; @@ -52,16 +54,23 @@ export default async function CountryLocaleLayout({ redirect(`/${fallbackCountry}/${fallbackLocale}`); } + const messages = await getMessages({ locale: locale as "en" | "de" | "pl" }); + return ( - - - - {children} - - + + + + {children} + + + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d63eccc8..6448185d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,6 @@ import { GoogleTagManager } from "@next/third-parties/google"; import type { Metadata } from "next"; import { Geist } from "next/font/google"; -import { NextIntlClientProvider } from "next-intl"; -import { getLocale, getMessages } from "next-intl/server"; import "./globals.css"; import { Suspense } from "react"; import { CartDrawer } from "@/components/cart/CartDrawer"; @@ -27,29 +25,24 @@ export const metadata: Metadata = { description: getStoreDescription(), }; -export default async function RootLayout({ +export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const locale = await getLocale(); - const messages = await getMessages(); - return ( - + {gtmId && } - - - - {children} - - - - - + + + {children} + + + + ); diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 04b3f67d..f84b5eaa 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -7,19 +7,25 @@ function isValidLocale(value: string | undefined): value is Locale { return !!value && supportedLocales.includes(value as Locale); } -export default getRequestConfig(async ({ requestLocale }) => { - // Prefer the route-derived locale (from the [locale] path segment). - // Default to "en" if the route segment is invalid or missing. - const requested = (await requestLocale) as string | undefined; - const locale: Locale = isValidLocale(requested) ? requested : "en"; +export default getRequestConfig(async ({ locale, requestLocale }) => { + // 1. Use explicit locale if provided (e.g. getMessages({ locale: 'en' })) + // 2. Fall back to requestLocale from the [locale] route segment + // 3. Default to "en" + let resolvedLocale: Locale; + + if (isValidLocale(locale)) { + resolvedLocale = locale; + } else { + const requested = await requestLocale; + resolvedLocale = isValidLocale(requested) ? requested : "en"; + } let messages: IntlMessages; try { - messages = (await import(`../../messages/${locale}.json`)).default; + messages = (await import(`../../messages/${resolvedLocale}.json`)).default; } catch { - // Fallback to English if locale file doesn't exist messages = (await import("../../messages/en.json")).default; } - return { locale, messages }; + return { locale: resolvedLocale, messages }; }); From b7af700f142d09235f3051a39cf9d0b933dcbfb0 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 19:42:08 +0200 Subject: [PATCH 17/28] Move CartProvider, CartDrawer, Toaster into [locale] layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CartDrawer uses useTranslations which requires NextIntlClientProvider context. Since the provider was moved to [country]/[locale]/layout.tsx, CartProvider/CartDrawer/Toaster must also live inside that layout — not in the root layout which is outside the i18n boundary. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/[country]/[locale]/layout.tsx | 30 ++++++++++++++++++--------- src/app/layout.tsx | 12 +---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/app/[country]/[locale]/layout.tsx b/src/app/[country]/[locale]/layout.tsx index 540152a4..487079b2 100644 --- a/src/app/[country]/[locale]/layout.tsx +++ b/src/app/[country]/[locale]/layout.tsx @@ -2,8 +2,12 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; +import { Suspense } from "react"; +import { CartDrawer } from "@/components/cart/CartDrawer"; import { JsonLd } from "@/components/seo/JsonLd"; +import { Toaster } from "@/components/ui/sonner"; import { AuthProvider } from "@/contexts/AuthContext"; +import { CartProvider } from "@/contexts/CartContext"; import { StoreProvider } from "@/contexts/StoreContext"; import { getMarkets } from "@/lib/data/markets"; import { generateStoreMetadata } from "@/lib/metadata/store"; @@ -61,16 +65,22 @@ export default async function CountryLocaleLayout({ messages={messages} locale={locale as "en" | "de" | "pl"} > - - - - {children} - - + + + + + + {children} + + + + + + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6448185d..365dd215 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,10 +2,6 @@ import { GoogleTagManager } from "@next/third-parties/google"; import type { Metadata } from "next"; import { Geist } from "next/font/google"; import "./globals.css"; -import { Suspense } from "react"; -import { CartDrawer } from "@/components/cart/CartDrawer"; -import { Toaster } from "@/components/ui/sonner"; -import { CartProvider } from "@/contexts/CartContext"; import { getStoreDescription, getStoreName } from "@/lib/seo"; const gtmId = process.env.GTM_ID; @@ -36,13 +32,7 @@ export default function RootLayout({ - - - {children} - - - - + {children} ); From 57885533c4d33ce3a630ffb87559369322a857db Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 20:04:47 +0200 Subject: [PATCH 18/28] Fix production build: match main's layout structure for PPR compatibility Root cause: next-intl's getMessages() in [locale] layout caused all pages to fail prerendering with "Uncached data accessed outside ". Fix: - Root layout: restore main's structure (Suspense > CartProvider > CartDrawer + Toaster). CartDrawer reverted to main's version (no i18n) since it renders outside NextIntlClientProvider. - [locale] layout: load translation JSON via static imports instead of getMessages() to avoid runtime data access during prerender. - Restore next-intl plugin in next.config.ts. Build succeeds with all pages using Partial Prerender. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/[country]/[locale]/layout.tsx | 43 +++++++++++++-------------- src/app/layout.tsx | 19 +++++++++--- src/components/cart/CartDrawer.tsx | 32 ++++++++------------ 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/app/[country]/[locale]/layout.tsx b/src/app/[country]/[locale]/layout.tsx index 487079b2..ff12c3d2 100644 --- a/src/app/[country]/[locale]/layout.tsx +++ b/src/app/[country]/[locale]/layout.tsx @@ -1,21 +1,25 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { NextIntlClientProvider } from "next-intl"; -import { getMessages } from "next-intl/server"; -import { Suspense } from "react"; -import { CartDrawer } from "@/components/cart/CartDrawer"; import { JsonLd } from "@/components/seo/JsonLd"; -import { Toaster } from "@/components/ui/sonner"; import { AuthProvider } from "@/contexts/AuthContext"; -import { CartProvider } from "@/contexts/CartContext"; import { StoreProvider } from "@/contexts/StoreContext"; import { getMarkets } from "@/lib/data/markets"; import { generateStoreMetadata } from "@/lib/metadata/store"; import { buildOrganizationJsonLd } from "@/lib/seo"; +import deMessages from "../../../../messages/de.json"; +import enMessages from "../../../../messages/en.json"; +import plMessages from "../../../../messages/pl.json"; const DEFAULT_COUNTRY = process.env.NEXT_PUBLIC_DEFAULT_COUNTRY || "us"; const DEFAULT_LOCALE = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || "en"; +const messagesMap: Record = { + en: enMessages, + de: deMessages, + pl: plMessages, +}; + interface CountryLocaleLayoutProps { children: React.ReactNode; params: Promise<{ @@ -58,29 +62,24 @@ export default async function CountryLocaleLayout({ redirect(`/${fallbackCountry}/${fallbackLocale}`); } - const messages = await getMessages({ locale: locale as "en" | "de" | "pl" }); + // Load messages statically (no runtime data access) to avoid blocking prerender + const messages = messagesMap[locale] || messagesMap.en; return ( - - - - - - {children} - - - - - - + + + + {children} + + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 365dd215..113cbacf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,10 @@ import { GoogleTagManager } from "@next/third-parties/google"; import type { Metadata } from "next"; import { Geist } from "next/font/google"; import "./globals.css"; -import { getStoreDescription, getStoreName } from "@/lib/seo"; +import { Suspense } from "react"; +import { CartDrawer } from "@/components/cart/CartDrawer"; +import { Toaster } from "@/components/ui/sonner"; +import { CartProvider } from "@/contexts/CartContext"; const gtmId = process.env.GTM_ID; @@ -11,14 +14,16 @@ const geist = Geist({ subsets: ["latin"], }); -const rootStoreName = getStoreName(); +const rootStoreName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; export const metadata: Metadata = { title: { template: `%s | ${rootStoreName}`, default: rootStoreName, }, - description: getStoreDescription(), + description: + process.env.NEXT_PUBLIC_STORE_DESCRIPTION || + "A modern e-commerce storefront powered by Spree Commerce and Next.js.", }; export default function RootLayout({ @@ -32,7 +37,13 @@ export default function RootLayout({ - {children} + + + {children} + + + + ); diff --git a/src/components/cart/CartDrawer.tsx b/src/components/cart/CartDrawer.tsx index 38b92500..a499f9ad 100644 --- a/src/components/cart/CartDrawer.tsx +++ b/src/components/cart/CartDrawer.tsx @@ -3,7 +3,6 @@ import { ShoppingBag, Trash, X } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useTranslations } from "next-intl"; import { useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; @@ -30,8 +29,6 @@ export function CartDrawer() { removeItem, itemCount, } = useCart(); - const t = useTranslations("cart"); - const tc = useTranslations("common"); const pathname = usePathname(); const basePath = extractBasePath(pathname); const viewCartFiredRef = useRef(false); @@ -80,18 +77,16 @@ export function CartDrawer() { - {t("cart")} + Cart {itemCount > 0 && ( - - {t("itemCount", { count: itemCount })} - + ({itemCount} items) )} @@ -115,14 +110,13 @@ export function CartDrawer() { className="w-16 h-16 text-gray-300 mb-4" strokeWidth={1} /> -

    {t("emptyCart")}

    -

    {t("emptyCartDescription")}

    +

    Your cart is empty

    - {tc("continueShopping")} + Continue Shopping
    ) : ( @@ -165,7 +159,7 @@ export function CartDrawer() { } }} disabled={updating} - aria-label={t("removeItemLabel", { name: item.name })} + aria-label="Remove item" > @@ -224,20 +218,18 @@ export function CartDrawer() { {/* Summary */}
    - {tc("subtotal")} + Subtotal {cart?.display_item_total}
    {cart?.discount_total && parseFloat(cart.discount_total) < 0 && (
    - {tc("discount")} + Discount {cart.display_discount_total}
    )}
    - {tc("shipping")} - - {t("shippingCalculatedAtCheckout")} - + Shipping + Calculated on checkout
    @@ -248,12 +240,12 @@ export function CartDrawer() { href={`${basePath}/checkout/${cart?.id}`} onClick={closeCart} > - {t("checkout")} + Checkout
    From b8eee8c25d1c4b3476c81c4b79df0db252c2ffa4 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 20:14:08 +0200 Subject: [PATCH 19/28] Regenerate package-lock.json with next-intl dependencies The lockfile from main was missing next-intl, @swc/helpers, tsx and their transitive dependencies, causing npm ci to fail in CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 828 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 799 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4824647..cb41077a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,12 @@ "@spree/sdk": "^0.20.1", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", + "@swc/helpers": "^0.5.19", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", "next": "^16", + "next-intl": "^4.8.3", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19", @@ -45,6 +47,7 @@ "lefthook": "^2.1.2", "react-email": "^5.2.10", "tailwindcss": "^4", + "tsx": "^4.19.0", "typescript": "^5", "vitest": "^4.0.18" } @@ -1094,7 +1097,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1653,6 +1655,57 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@formatjs/bigdecimal": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.0.tgz", + "integrity": "sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==", + "license": "MIT" + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.2.0.tgz", + "integrity": "sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==", + "license": "MIT", + "dependencies": { + "@formatjs/bigdecimal": "0.2.0", + "@formatjs/fast-memoize": "3.1.1", + "@formatjs/intl-localematcher": "0.8.2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.1.tgz", + "integrity": "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.3.tgz", + "integrity": "sha512-HJWZ9S6JWey6iY5+YXE3Kd0ofWU1sC2KTTp56e1168g/xxWvVvr8k9G4fexIgwYV9wbtjY7kGYK5FjoWB3B2OQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.2.0", + "@formatjs/icu-skeleton-parser": "2.1.3" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.3.tgz", + "integrity": "sha512-9mFp8TJ166ZM2pcjKwsBWXrDnOJGT7vMEScVgLygUODPOsE8S6f/FHoacvrlHK1B4dYZk8vSCNruyPU64AfgJQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.2.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.2.tgz", + "integrity": "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.1" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -1682,7 +1735,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1705,7 +1757,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1728,7 +1779,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1745,7 +1795,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1762,7 +1811,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1779,7 +1827,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1796,7 +1843,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1813,7 +1859,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1830,7 +1875,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1847,7 +1891,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1864,7 +1907,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1881,7 +1923,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1898,7 +1939,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1921,7 +1961,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1944,7 +1983,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1967,7 +2005,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1990,7 +2027,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2013,7 +2049,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2036,7 +2071,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2059,7 +2093,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2082,7 +2115,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2102,7 +2134,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2122,7 +2153,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2142,7 +2172,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3124,6 +3153,319 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@prisma/instrumentation": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.2.0.tgz", @@ -5037,6 +5379,16 @@ "node": ">= 10" } }, + "node_modules/@react-email/preview-server/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@react-email/preview-server/node_modules/next": { "version": "16.1.7", "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", @@ -5617,6 +5969,12 @@ "win32" ] }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -6163,15 +6521,280 @@ "node": ">=12.16" } }, + "node_modules/@swc/core": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz", + "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.24", + "@swc/core-darwin-x64": "1.15.24", + "@swc/core-linux-arm-gnueabihf": "1.15.24", + "@swc/core-linux-arm64-gnu": "1.15.24", + "@swc/core-linux-arm64-musl": "1.15.24", + "@swc/core-linux-ppc64-gnu": "1.15.24", + "@swc/core-linux-s390x-gnu": "1.15.24", + "@swc/core-linux-x64-gnu": "1.15.24", + "@swc/core-linux-x64-musl": "1.15.24", + "@swc/core-win32-arm64-msvc": "1.15.24", + "@swc/core-win32-ia32-msvc": "1.15.24", + "@swc/core-win32-x64-msvc": "1.15.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz", + "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz", + "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz", + "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz", + "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz", + "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz", + "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz", + "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz", + "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz", + "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz", + "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz", + "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz", + "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -8209,7 +8832,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -9169,6 +9791,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -9421,6 +10056,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icu-minify": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.9.0.tgz", + "integrity": "sha512-9ev7MqkN29jcIelUAqJRfNCxzGOEkBJPnr+scYATMp2bfpU4Bm1eIwYU0/o5xRy8BBnSWMUjK58WTB3132P0bg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9474,6 +10124,17 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/intl-messageformat": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.0.tgz", + "integrity": "sha512-IhghAA8n4KSlXuWKzYsWyWb82JoYTzShfyvdSF85oJPnNOjvv4kAo7S7Jtkm3/vJ53C7dQNRO+Gpnj3iWgTjBQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.2.0", + "@formatjs/fast-memoize": "3.1.1", + "@formatjs/icu-messageformat-parser": "3.5.3" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -10783,6 +11444,43 @@ } } }, + "node_modules/next-intl": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.9.0.tgz", + "integrity": "sha512-MMNAjewHUw9Ke93E5/Yzhf8lqesesaXJTPlrK3FwECgn4EXG9m7Tuzy4rnDes0ogjDhQIa/Ksj/qmFnHJAOluw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.9.0", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.9.0", + "po-parser": "^2.1.1", + "use-intl": "^4.9.0" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.9.0.tgz", + "integrity": "sha512-CAu6Qy6XiCenKsvzyCPm2cZFkGfcvhJi8N93TCnOowmzD4Br3ked7QdROusRRp4MQ1iG9u+KCLgVcM9CLDUOIQ==", + "license": "MIT" + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -10793,6 +11491,15 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -10821,6 +11528,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -11340,6 +12053,12 @@ "pathe": "^2.0.3" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/postal-mime": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", @@ -12050,6 +12769,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -13247,6 +13976,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", @@ -13437,6 +14186,27 @@ } } }, + "node_modules/use-intl": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.9.0.tgz", + "integrity": "sha512-GehJvP7gu8SvmaDHNDNrRHt2TCNSZt4l1cGJMpUX77TGeZPAQKVQokAVvoYkeTT1UWPtv9RJ6N16UJNButzrgg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.9.0", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", From dc69c83b961ce520658e7cbc3025b17a366a1568 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 20:29:01 +0200 Subject: [PATCH 20/28] Fix server components using wrong locale for translations All server components using getTranslations() were defaulting to "en" because they called getTranslations("namespace") without passing the locale explicitly. The requestLocale from next-intl's config was not resolving correctly. Fix: pass locale explicitly to all getTranslations() calls: - Page components: extract locale from params, pass as getTranslations({ locale, namespace }) - Header/Footer: added locale prop, passed from storefront layout Co-Authored-By: Claude Opus 4.6 (1M context) --- .../(storefront)/account/addresses/page.tsx | 12 ++++++++++-- .../(storefront)/account/credit-cards/page.tsx | 14 ++++++++++++-- .../(storefront)/account/gift-cards/page.tsx | 12 ++++++++++-- .../(storefront)/account/orders/[id]/page.tsx | 5 ++++- .../[locale]/(storefront)/account/orders/page.tsx | 5 ++++- src/app/[country]/[locale]/(storefront)/layout.tsx | 12 ++++++++++-- src/app/[country]/[locale]/(storefront)/page.tsx | 5 ++++- .../[locale]/(storefront)/policies/[slug]/page.tsx | 11 +++++++---- src/components/layout/Footer.tsx | 9 +++++++-- src/components/layout/Header.tsx | 9 +++++++-- src/types/next-intl.d.ts | 10 +++++----- 11 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx b/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx index 8f3eaac6..a1805763 100644 --- a/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/addresses/page.tsx @@ -7,9 +7,17 @@ import { getAddresses } from "@/lib/data/addresses"; import { getCountries } from "@/lib/data/countries"; import { getCustomer } from "@/lib/data/customer"; -export default async function AddressesPage() { +interface AddressesPageProps { + params: Promise<{ country: string; locale: string }>; +} + +export default async function AddressesPage({ params }: AddressesPageProps) { await connection(); - const t = await getTranslations("account"); + const { locale } = await params; + const t = await getTranslations({ + locale: locale as Locale, + namespace: "account", + }); const [addressResponse, countriesResponse, customer] = await Promise.all([ getAddresses(), getCountries(), diff --git a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx index 0d255d66..6ae6099a 100644 --- a/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/credit-cards/page.tsx @@ -4,9 +4,19 @@ import { getTranslations } from "next-intl/server"; import { CreditCardList } from "@/components/account/CreditCardList"; import { getCreditCards } from "@/lib/data/credit-cards"; -export default async function CreditCardsPage() { +interface CreditCardsPageProps { + params: Promise<{ country: string; locale: string }>; +} + +export default async function CreditCardsPage({ + params, +}: CreditCardsPageProps) { await connection(); - const t = await getTranslations("creditCards"); + const { locale } = await params; + const t = await getTranslations({ + locale: locale as Locale, + namespace: "creditCards", + }); const response = await getCreditCards(); const cards = response.data; diff --git a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx index 9a621dd8..7331b23e 100644 --- a/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx @@ -4,9 +4,17 @@ import { getTranslations } from "next-intl/server"; import { GiftCardList } from "@/components/account/GiftCardList"; import { getGiftCards } from "@/lib/data/gift-cards"; -export default async function GiftCardsPage() { +interface GiftCardsPageProps { + params: Promise<{ country: string; locale: string }>; +} + +export default async function GiftCardsPage({ params }: GiftCardsPageProps) { await connection(); - const t = await getTranslations("giftCards"); + const { locale } = await params; + const t = await getTranslations({ + locale: locale as Locale, + namespace: "giftCards", + }); const response = await getGiftCards(); const cards = response.data; diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index 2d14c433..4f9b3550 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -16,8 +16,11 @@ export default async function OrderDetailPage({ params, }: OrderDetailPageProps) { await connection(); - const t = await getTranslations("orders"); const { country, locale, id } = await params; + const t = await getTranslations({ + locale: locale as Locale, + namespace: "orders", + }); const basePath = `/${country}/${locale}`; const order = await getOrder(id); diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx index e2ec8d2f..c4c962b9 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -12,8 +12,11 @@ interface OrdersPageProps { export default async function OrdersPage({ params }: OrdersPageProps) { await connection(); - const t = await getTranslations("orders"); const { country, locale } = await params; + const t = await getTranslations({ + locale: locale as Locale, + namespace: "orders", + }); const basePath = `/${country}/${locale}`; const response = await getOrders({ limit: 50 }); diff --git a/src/app/[country]/[locale]/(storefront)/layout.tsx b/src/app/[country]/[locale]/(storefront)/layout.tsx index 14c25316..3b7729cd 100644 --- a/src/app/[country]/[locale]/(storefront)/layout.tsx +++ b/src/app/[country]/[locale]/(storefront)/layout.tsx @@ -45,14 +45,22 @@ export default async function StorefrontLayout({ return ( <> -
    +
    {rootCategories.length > 0 && ( )}
    {children}
    -
    +
    ); } diff --git a/src/app/[country]/[locale]/(storefront)/page.tsx b/src/app/[country]/[locale]/(storefront)/page.tsx index ce94e714..893a79f8 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -23,7 +23,10 @@ export async function generateMetadata({ export default async function HomePage({ params }: HomePageProps) { const { country, locale } = await params; const basePath = `/${country}/${locale}`; - const t = await getTranslations("home"); + const t = await getTranslations({ + locale: locale as Locale, + namespace: "home", + }); const storeName = getStoreName(); return ( diff --git a/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx b/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx index d8243043..9b6dd26c 100644 --- a/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx @@ -15,11 +15,14 @@ interface PolicyPageProps { export async function generateMetadata({ params, }: PolicyPageProps): Promise { - const { slug } = await params; + const { slug, locale } = await params; const policy = await getPolicy(slug); if (!policy) { - const t = await getTranslations("policies"); + const t = await getTranslations({ + locale: locale as Locale, + namespace: "policies", + }); return { title: t("policyNotFound") }; } @@ -31,10 +34,10 @@ export async function generateMetadata({ } export default async function PolicyPage({ params }: PolicyPageProps) { - const { slug } = await params; + const { slug, locale } = await params; const [policy, t] = await Promise.all([ getPolicy(slug), - getTranslations("policies"), + getTranslations({ locale: locale as Locale, namespace: "policies" }), ]); if (!policy) { diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 7c539526..2c9dbff8 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -11,10 +11,15 @@ const storeDescription = interface FooterProps { rootCategories: Category[]; basePath: string; + locale: Locale; } -export async function Footer({ rootCategories, basePath }: FooterProps) { - const t = await getTranslations("footer"); +export async function Footer({ + rootCategories, + basePath, + locale, +}: FooterProps) { + const t = await getTranslations({ locale, namespace: "footer" }); return (
    diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ef3c4ac1..6362f18b 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -14,10 +14,15 @@ const storeName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; interface HeaderProps { rootCategories: Category[]; basePath: string; + locale: Locale; } -export async function Header({ rootCategories, basePath }: HeaderProps) { - const t = await getTranslations("header"); +export async function Header({ + rootCategories, + basePath, + locale, +}: HeaderProps) { + const t = await getTranslations({ locale, namespace: "header" }); return ( Date: Tue, 7 Apr 2026 20:35:17 +0200 Subject: [PATCH 21/28] Add Spanish (es) and French (fr) locale support - Created messages/es.json and messages/fr.json with full translations - Updated Locale type to include "es" | "fr" - Added es/fr to supportedLocales in i18n/request.ts - Added es/fr message imports to [locale] layout Co-Authored-By: Claude Opus 4.6 (1M context) --- messages/es.json | 498 ++++++++++++++++++++++++++ messages/fr.json | 498 ++++++++++++++++++++++++++ src/app/[country]/[locale]/layout.tsx | 4 + src/i18n/request.ts | 2 +- src/types/next-intl.d.ts | 2 +- 5 files changed, 1002 insertions(+), 2 deletions(-) create mode 100644 messages/es.json create mode 100644 messages/fr.json diff --git a/messages/es.json b/messages/es.json new file mode 100644 index 00000000..9d0c1b71 --- /dev/null +++ b/messages/es.json @@ -0,0 +1,498 @@ +{ + "common": { + "subtotal": "Subtotal", + "shipping": "Envio", + "tax": "Impuestos", + "discount": "Descuento", + "total": "Total", + "adjustments": "Ajustes", + "edit": "Editar", + "remove": "Eliminar", + "cancel": "Cancelar", + "back": "Volver", + "continue": "Continuar", + "loading": "Cargando...", + "apply": "Aplicar", + "saving": "Guardando...", + "processing": "Procesando...", + "continueShopping": "Seguir comprando", + "orderSummary": "Resumen del pedido", + "selectCountry": "Seleccionar pais", + "myAccount": "Mi cuenta", + "giftCard": "Tarjeta regalo", + "storeCredit": "Credito de la tienda", + "amountDue": "Importe pendiente" + }, + "header": { + "categories": "Categorias", + "openCart": "Abrir carrito", + "account": "Cuenta", + "closeMenu": "Cerrar menu", + "openMenu": "Abrir menu", + "menu": "Menu", + "selectCountry": "Seleccionar pais", + "home": "Inicio", + "allProducts": "Todos los productos", + "contact": "Contacto", + "viewAllCategory": "Ver todo en {category}", + "openSearch": "Abrir busqueda", + "closeSearch": "Cerrar busqueda", + "myAccount": "Mi cuenta" + }, + "footer": { + "description": "Una tienda en linea moderna impulsada por Spree Commerce y Next.js.", + "shop": "Tienda", + "allProducts": "Todos los productos", + "categories": "Categorias", + "account": "Cuenta", + "myAccount": "Mi cuenta", + "orderHistory": "Historial de pedidos", + "cart": "Carrito", + "policies": "Politicas", + "poweredBy": "Impulsado por", + "copyright": "\u00a9 {year} {storeName}. Impulsado por Spree Commerce." + }, + "home": { + "welcome": "Bienvenido a {storeName}", + "heroDescription": "Descubre productos increibles con nuestra experiencia de comercio electronico moderna impulsada por Spree Commerce.", + "shopNow": "Comprar ahora", + "browseCategories": "Explorar categorias", + "featuredProducts": "Productos destacados", + "viewAll": "Ver todo", + "qualityProducts": "Productos de calidad", + "qualityDescription": "Una seleccion cuidadosamente curada de los mejores productos.", + "fastShipping": "Envio rapido", + "shippingDescription": "Entrega rapida y fiable hasta tu puerta.", + "support": "Soporte 24/7", + "supportDescription": "Nuestro equipo esta aqui para ayudarte en cualquier momento." + }, + "cart": { + "cart": "Carrito", + "closeCart": "Cerrar carrito", + "emptyCart": "Tu carrito esta vacio", + "emptyCartDescription": "Parece que aun no has agregado nada a tu carrito.", + "removeItem": "Eliminar articulo", + "decreaseQuantity": "Disminuir cantidad", + "increaseQuantity": "Aumentar cantidad", + "shippingNote": "Envio e impuestos calculados en el checkout", + "checkout": "Pagar", + "viewCart": "Ver carrito", + "shoppingCart": "Carrito de compras", + "proceedToCheckout": "Proceder al pago", + "itemCount": "({count, plural, one {# articulo} other {# articulos}})", + "shippingCalculatedAtCheckout": "Calculado en el checkout", + "removeItemLabel": "Eliminar {name}", + "giftCard": "Tarjeta regalo", + "storeCredit": "Credito de la tienda", + "amountDue": "Importe pendiente" + }, + "products": { + "allProducts": "Todos los productos", + "browseCollection": "Explora nuestra coleccion completa", + "searchResultsFor": "Resultados de busqueda para \"{query}\"", + "productsFound": "{count, plural, one {# producto} other {# productos}} encontrados", + "noProductsFound": "No se encontraron productos", + "tryAdjustingFilters": "Intenta ajustar tus filtros", + "noMatchingProducts": "No encontramos productos que coincidan con \"{query}\"", + "showingProducts": "Mostrando {shown} de {total} productos", + "loadingMore": "Cargando mas...", + "noMoreProducts": "No hay mas productos para cargar", + "filters": "Filtros", + "sortBy": "Ordenar por", + "resetFilters": "Restablecer filtros", + "price": "Precio", + "availability": "Disponibilidad", + "inStock": "En stock", + "outOfStock": "Agotado", + "min": "Min", + "max": "Max", + "range": "Rango: {min} - {max}", + "clear": "Limpiar", + "productCount": "{count, plural, one {# producto} other {# productos}}", + "sale": "Oferta", + "outOfStockVariant": "(Agotado)", + "addToCart": "Agregar al carrito", + "adding": "Agregando...", + "description": "Descripcion", + "details": "Detalles", + "sku": "SKU", + "options": "Opciones", + "search": "Buscar...", + "searching": "Buscando...", + "viewAllResultsFor": "Ver todos los resultados para \"{query}\"", + "manual": "Manual", + "bestSelling": "Mas vendidos", + "priceLowHigh": "Precio (menor a mayor)", + "priceHighLow": "Precio (mayor a menor)", + "newest": "Mas recientes", + "oldest": "Mas antiguos", + "nameAZ": "Nombre (A-Z)", + "nameZA": "Nombre (Z-A)", + "sortUnknown": "Otro", + "categories": "Categorias", + "noCategoriesFound": "No se encontraron categorias.", + "noCategoriesInGroup": "No hay categorias en este grupo.", + "subcategories": "{count, plural, one {# subcategoria} other {# subcategorias}}", + "noImageAvailable": "Imagen no disponible", + "clickToZoom": "Clic para ampliar", + "noProductsMatchingFilters": "No se encontraron productos que coincidan con tus filtros.", + "decreaseQuantity": "Disminuir cantidad", + "increaseQuantity": "Aumentar cantidad", + "carouselPrev": "Productos anteriores", + "carouselNext": "Productos siguientes", + "lightboxClose": "Cerrar visor", + "lightboxPrev": "Imagen anterior", + "lightboxNext": "Imagen siguiente", + "subcategoriesHeading": "Subcategorias", + "sort": "Ordenar", + "clearAll": "Limpiar todo", + "clearAllFiltersCount": "Limpiar todos los filtros ({count})", + "showResults": "Mostrar resultados", + "closeFilters": "Cerrar filtros", + "anyAvailability": "Cualquier disponibilidad", + "customPrice": "Personalizado", + "priceLabel": "Precio: {value}", + "clearFilter": "Limpiar {label}", + "openImageZoom": "Abrir zoom de imagen", + "priceUnder": "Menos de {price}", + "priceAbove": "{price}+", + "priceRangeBucket": "{min} \u2013 {max}", + "properties": "Propiedades", + "yes": "Si", + "no": "No" + }, + "navigation": { + "home": "Inicio", + "categories": "Categorias", + "breadcrumb": "Migas de pan" + }, + "checkout": { + "checkout": "Pagar", + "orderNumber": "Pedido #{number}", + "checkoutError": "Error en el pago", + "returnToCart": "Volver al carrito", + "emptyCart": "Tu carrito esta vacio", + "emptyCartDescription": "Agrega algunos articulos a tu carrito antes de pagar.", + "stepShipping": "Envio", + "stepDelivery": "Entrega", + "stepPayment": "Pago", + "contactInformation": "Informacion de contacto", + "emailAddress": "Correo electronico", + "emailPlaceholder": "tu@ejemplo.com", + "usingAccountEmail": "Usando el correo electronico de tu cuenta", + "signInPrompt": "\u00bfYa tienes una cuenta?", + "signIn": "Iniciar sesion", + "signInDescription": "para acceder a tus direcciones guardadas e historial de pedidos.", + "shippingAddress": "Direccion de envio", + "continueToDelivery": "Continuar a entrega", + "shippingMethod": "Metodo de envio", + "loadingShippingOptions": "Cargando opciones de envio...", + "enterShippingAddressForMethods": "Ingresa tu direccion de envio para ver los metodos de envio disponibles.", + "shipmentOf": "Envio {current} de {total}", + "shipsFrom": "Se envia desde {location}", + "phone": "Telefono: {phone}", + "billingAddress": "Direccion de facturacion", + "sameAsShipping": "Igual que la direccion de envio", + "paymentMethod": "Metodo de pago", + "loadingPaymentForm": "Cargando formulario de pago...", + "addNewPaymentMethod": "Agregar nuevo metodo de pago", + "endingIn": "terminada en", + "exp": "Venc.", + "savedCardLabel": "{brand} terminada en {digits}", + "cardExpiry": "Venc. {month}/{year}", + "default": "Predeterminado", + "noPaymentMethods": "No hay metodos de pago disponibles para este pedido.", + "secureTransactions": "Todas las transacciones son seguras y estan encriptadas.", + "creditCard": "Tarjeta de credito", + "enterShippingAddress": "Ingresa la direccion de envio", + "payNow": "Pagar ahora", + "failedToInitPayment": "Error al inicializar el pago. Por favor, intenta de nuevo.", + "failedToCreateSession": "Error al crear la sesion de pago.", + "paymentError": "Ocurrio un error durante el pago. Por favor, intenta de nuevo.", + "orderNotFound": "Pedido no encontrado o no tienes acceso.", + "failedToLoadCheckout": "Error al cargar el checkout. Por favor, intenta de nuevo.", + "orderNotFoundAfterPayment": "Pedido no encontrado despues del pago. Por favor, contacta a soporte.", + "failedToSaveAddress": "Error al guardar la direccion", + "failedToProceed": "Error al continuar al siguiente paso", + "generalError": "Ocurrio un error. Por favor, intenta de nuevo.", + "failedToSelectRate": "Error al seleccionar la tarifa de envio", + "failedToSaveBilling": "Error al guardar la direccion de facturacion", + "failedToSaveBillingRetry": "Error al guardar la direccion de facturacion. Por favor, intenta de nuevo.", + "failedToCompleteOrder": "Error al completar el pedido", + "stripeNotLoaded": "Stripe aun no se ha cargado", + "paymentProcessingError": "Ocurrio un error durante el pago.", + "failedToCompletePaymentSession": "Error al completar la sesion de pago", + "noOrder": "Sin pedido", + "confirmingPayment": "Confirmando tu pago...", + "policyConsentRequired": "Debes aceptar las politicas de la tienda antes de realizar tu pedido", + "iAgreeToThe": "Acepto las", + "policySeparatorComma": ", ", + "policySeparatorAnd": "y" + }, + "address": { + "firstName": "Nombre", + "lastName": "Apellido", + "company": "Empresa (opcional)", + "address": "Direccion", + "streetAddress": "Direccion", + "apartment": "Apartamento, suite, etc. (opcional)", + "city": "Ciudad", + "country": "Pais", + "selectCountry": "Selecciona un pais", + "stateProvince": "Estado / Provincia", + "selectState": "Selecciona un estado", + "stateOrProvince": "Estado o provincia", + "zipCode": "Codigo postal", + "phone": "Telefono (opcional)", + "selectAnAddress": "Selecciona una direccion", + "useDifferentAddress": "Usar una direccion diferente", + "editAddress": "Editar direccion", + "addNewAddress": "Agregar nueva direccion", + "saveAddress": "Guardar direccion", + "failedToSave": "Error al guardar la direccion", + "deleteAddressTitle": "\u00bfEliminar direccion?", + "deleteAddressConfirmation": "Esto eliminara permanentemente esta direccion. Esta accion no se puede deshacer.", + "delete": "Eliminar", + "deleting": "Eliminando...", + "cancel": "Cancelar", + "edit": "Editar", + "failedToDeleteAddress": "Error al eliminar la direccion", + "addFirstAddress": "Agrega tu primera direccion", + "addAddress": "Agregar direccion" + }, + "account": { + "myAccount": "Mi cuenta", + "signInDescription": "Inicia sesion para acceder a tu cuenta e historial de pedidos.", + "email": "Correo electronico", + "password": "Contrasena", + "signIn": "Iniciar sesion", + "signingIn": "Iniciando sesion...", + "invalidCredentials": "Correo electronico o contrasena invalidos", + "dontHaveAccount": "\u00bfNo tienes una cuenta?", + "signUp": "Registrate", + "accountOverview": "Resumen de la cuenta", + "orderHistory": "Historial de pedidos", + "orderHistoryDescription": "Consulta tus pedidos anteriores y rastrea envios", + "addresses": "Direcciones", + "addressesDescription": "Administra tus direcciones de envio y facturacion", + "paymentMethods": "Metodos de pago", + "paymentMethodsDescription": "Administra tus tarjetas de credito guardadas", + "profile": "Perfil", + "profileDescription": "Actualiza tu informacion personal", + "signOut": "Cerrar sesion", + "overview": "Resumen", + "orders": "Pedidos", + "giftCards": "Tarjetas regalo", + "addNewAddress": "Agregar nueva direccion", + "noAddresses": "No hay direcciones guardadas", + "noAddressesDescription": "Las direcciones que agregues durante el checkout apareceran aqui.", + "defaultAddress": "Predeterminada", + "deleteConfirm": "\u00bfEstas seguro de que deseas eliminar esta direccion?", + "setAsDefault": "Establecer como predeterminada", + "showPassword": "Mostrar contrasena", + "hidePassword": "Ocultar contrasena", + "forgotPassword": "\u00bfOlvidaste tu contrasena?", + "policyConsentRequired": "Debes aceptar las politicas de la tienda para crear una cuenta" + }, + "register": { + "createAccount": "Crear cuenta", + "signUpDescription": "Registrate para empezar a comprar con nosotros.", + "confirmPassword": "Confirmar contrasena", + "creatingAccount": "Creando cuenta...", + "passwordsDontMatch": "Las contrasenas no coinciden", + "passwordTooShort": "La contrasena debe tener al menos 6 caracteres", + "registrationFailed": "Error en el registro. Por favor, intenta de nuevo.", + "alreadyHaveAccount": "\u00bfYa tienes una cuenta?", + "signIn": "Iniciar sesion", + "unexpectedError": "Ocurrio un error inesperado. Por favor, intenta de nuevo.", + "firstName": "Nombre", + "lastName": "Apellido", + "firstNamePlaceholder": "Juan", + "lastNamePlaceholder": "Garcia", + "emailPlaceholder": "tu@ejemplo.com" + }, + "orders": { + "orderHistory": "Historial de pedidos", + "noOrders": "Aun no hay pedidos", + "noOrdersDescription": "Cuando realices pedidos, apareceran aqui.", + "startShopping": "Empezar a comprar", + "order": "Pedido", + "date": "Fecha", + "status": "Estado", + "actions": "Acciones", + "view": "Ver", + "orderNotFound": "Pedido no encontrado", + "backToOrders": "Volver a pedidos", + "placedOn": "Realizado el {date}", + "orderItems": "Articulos del pedido", + "qty": "Cant.: {quantity}", + "orderAgain": "Pedir de nuevo", + "deliveryAddress": "Direccion de entrega", + "shippingMethod": "Metodo de envio", + "shippedFrom": "Enviado desde {location}", + "trackItems": "Rastrear articulos", + "shipmentCanceled": "Este envio ha sido cancelado.", + "noTrackingInfo": "No hay informacion de rastreo disponible.", + "paymentInformation": "Informacion de pago", + "cardEndingIn": "{label} terminada en {digits}", + "cardExpires": "Vence {month}/{year}", + "storeCreditApplied": "Aplicado {amount} \u2014 {remaining} restante", + "billingAddress": "Direccion de facturacion", + "orderNotFoundDescription": "El pedido que buscas no existe.", + "storeCredit": "Credito de la tienda", + "specialInstructions": "Instrucciones especiales", + "payment": "Pago", + "shipment": "Envio", + "shipped": "Enviado", + "partiallyShipped": "Parcialmente enviado", + "delivered": "Entregado", + "ready": "Listo", + "pending": "Pendiente", + "canceled": "Cancelado", + "backorder": "Pedido pendiente", + "paid": "Pagado", + "balanceDue": "Saldo pendiente", + "creditOwed": "Credito pendiente", + "failed": "Fallido", + "void": "Anulado", + "notAvailable": "N/D", + "unknownShipmentStatus": "Desconocido", + "orderTitle": "Pedido #{number}", + "shipmentCanceledRefund": "Envio cancelado \u2014 se ha emitido un reembolso.", + "shippingMethodUnavailable": "No disponible", + "totalColumn": "Total" + }, + "orderPlaced": { + "thanksForOrder": "\u00a1Gracias por tu pedido, {name}!", + "thanksForOrderAnonymous": "\u00a1Gracias por tu pedido!", + "orderNumber": "Pedido #{number}", + "emailConfirmation": "Recibiras una confirmacion por correo electronico en breve.", + "orderItems": "Articulos del pedido", + "contactInformation": "Informacion de contacto", + "shippingAddress": "Direccion de envio", + "billingAddress": "Direccion de facturacion", + "orderNotFound": "Pedido no encontrado", + "failedToLoad": "Error al cargar los detalles del pedido.", + "qty": "Cant.: {quantity}", + "shippingMethod": "Metodo de envio", + "standardShipping": "Envio estandar", + "payment": "Pago", + "giftCard": "Tarjeta regalo", + "confirmationSentTo": "Confirmacion enviada a" + }, + "coupon": { + "placeholder": "Tarjeta regalo o codigo de descuento", + "apply": "Aplicar", + "remove": "Eliminar", + "invalidCode": "Codigo de cupon invalido", + "applyFailed": "No se pudo aplicar el cupon. Por favor, intenta de nuevo.", + "failedToRemove": "Error al eliminar el codigo de cupon", + "failedToRemoveGiftCard": "Error al eliminar la tarjeta regalo", + "removeCoupon": "Eliminar cupon {code}", + "removeGiftCard": "Eliminar tarjeta regalo", + "giftCardCode": "Tarjeta regalo {code}", + "applying": "Aplicando...", + "removing": "Eliminando..." + }, + "checkoutLayout": { + "backToStore": "Volver a la tienda", + "showOrderSummary": "Mostrar resumen del pedido", + "hideOrderSummary": "Ocultar resumen del pedido", + "allRightsReserved": "\u00a9 {year} {storeName}. Todos los derechos reservados." + }, + "profile": { + "profile": "Perfil", + "firstName": "Nombre", + "lastName": "Apellido", + "emailAddress": "Correo electronico", + "saveChanges": "Guardar cambios", + "saving": "Guardando...", + "profileUpdated": "\u00a1Perfil actualizado con exito!", + "accountInformation": "Informacion de la cuenta", + "accountId": "ID de cuenta", + "loadingProfile": "Cargando perfil...", + "failedToUpdate": "Error al actualizar el perfil", + "email": "Correo electronico" + }, + "creditCards": { + "paymentMethods": "Metodos de pago", + "noCards": "No hay metodos de pago guardados", + "noCardsDescription": "Los metodos de pago se guardan automaticamente cuando realizas una compra.", + "expires": "Vence", + "default": "Predeterminado", + "removing": "Eliminando...", + "secureInfo": "Tu informacion de pago esta almacenada de forma segura.", + "deleteConfirm": "\u00bfEstas seguro de que deseas eliminar esta tarjeta?", + "cardMaskedLabel": "{label} terminada en {digits}, vence {month}/{year}", + "removePaymentMethodTitle": "\u00bfEliminar metodo de pago?", + "endingIn": "terminada en", + "cardEndingIn": "{label} terminada en {digits}", + "cardExpires": "Vence {month}/{year}", + "remove": "Eliminar", + "cancel": "Cancelar", + "confirmRemoveDescription": "Esto eliminara la {label} terminada en {digits} de tu cuenta. Esta accion no se puede deshacer.", + "failedToRemove": "Error al eliminar la tarjeta: {error}" + }, + "giftCards": { + "giftCards": "Tarjetas regalo", + "noGiftCards": "No hay tarjetas regalo", + "noGiftCardsDescription": "Aun no tienes tarjetas regalo.", + "active": "Activa", + "partiallyUsed": "Parcialmente usada", + "fullyRedeemed": "Totalmente canjeada", + "canceled": "Cancelada", + "expired": "Expirada", + "noExpiration": "Sin fecha de expiracion", + "remaining": "restante", + "usedAmount": "Usado: {amount}", + "totalAmountWithValue": "Total: {amount}", + "addedOnDate": "Agregada el {date}", + "fullyRedeemedOnDate": "Totalmente canjeada el {date}", + "activeGiftCards": "Tarjetas regalo activas", + "expiredRedeemed": "Expiradas / Canjeadas", + "copy": "Copiar", + "copied": "\u00a1Copiado!", + "copyCodeToClipboard": "Copiar codigo al portapapeles", + "expiresOn": "Expira el {date}", + "percentUsed": "{percent}% usado", + "activeGiftCardsCount": "Tarjetas regalo activas ({count})", + "expiredRedeemedCount": "Expiradas / Canjeadas ({count})", + "giftCardsHelpText": "Las tarjetas regalo se pueden usar durante el checkout para pagar tus pedidos. El saldo restante se guardara para futuras compras." + }, + "forgotPassword": { + "title": "Restablecer tu contrasena", + "description": "Ingresa tu correo electronico y te enviaremos un enlace para restablecer tu contrasena.", + "email": "Correo electronico", + "sending": "Enviando...", + "sendResetLink": "Enviar enlace de restablecimiento", + "backToSignIn": "Volver a iniciar sesion", + "checkYourEmail": "Revisa tu correo electronico", + "resetEmailSent": "Si existe una cuenta para {email}, hemos enviado instrucciones para restablecer la contrasena.", + "linkExpiry": "El enlace en el correo expirara en 15 minutos. Revisa tu carpeta de spam si no lo encuentras.", + "tryDifferentEmail": "Probar con otro correo electronico", + "genericError": "Algo salio mal. Por favor, intenta de nuevo." + }, + "resetPassword": { + "title": "Establecer una nueva contrasena", + "description": "Ingresa tu nueva contrasena a continuacion.", + "newPassword": "Nueva contrasena", + "confirmPassword": "Confirmar nueva contrasena", + "resetting": "Restableciendo...", + "resetPassword": "Restablecer contrasena", + "backToSignIn": "Volver a iniciar sesion", + "invalidLink": "Enlace invalido", + "invalidLinkDescription": "Este enlace de restablecimiento de contrasena es invalido o ha expirado.", + "requestNewLink": "Solicitar un nuevo enlace de restablecimiento", + "passwordsDontMatch": "Las contrasenas no coinciden", + "passwordTooShort": "La contrasena debe tener al menos 6 caracteres", + "linkExpired": "Este enlace de restablecimiento ha expirado. Por favor, solicita uno nuevo.", + "genericError": "Algo salio mal. Por favor, intenta de nuevo.", + "success": "Contrasena restablecida con exito", + "successDescription": "Tu contrasena ha sido actualizada. Ahora puedes iniciar sesion con tu nueva contrasena.", + "signIn": "Iniciar sesion" + }, + "policies": { + "policyNotFound": "Politica no encontrada", + "noContent": "No hay contenido disponible para esta politica." + } +} diff --git a/messages/fr.json b/messages/fr.json new file mode 100644 index 00000000..4c848b03 --- /dev/null +++ b/messages/fr.json @@ -0,0 +1,498 @@ +{ + "common": { + "subtotal": "Sous-total", + "shipping": "Livraison", + "tax": "Taxes", + "discount": "Remise", + "total": "Total", + "adjustments": "Ajustements", + "edit": "Modifier", + "remove": "Supprimer", + "cancel": "Annuler", + "back": "Retour", + "continue": "Continuer", + "loading": "Chargement...", + "apply": "Appliquer", + "saving": "Enregistrement...", + "processing": "Traitement...", + "continueShopping": "Continuer les achats", + "orderSummary": "Recapitulatif de la commande", + "selectCountry": "Choisir le pays", + "myAccount": "Mon compte", + "giftCard": "Carte cadeau", + "storeCredit": "Credit boutique", + "amountDue": "Montant du" + }, + "header": { + "categories": "Categories", + "openCart": "Ouvrir le panier", + "account": "Compte", + "closeMenu": "Fermer le menu", + "openMenu": "Ouvrir le menu", + "menu": "Menu", + "selectCountry": "Choisir le pays", + "home": "Accueil", + "allProducts": "Tous les produits", + "contact": "Contact", + "viewAllCategory": "Voir tout dans {category}", + "openSearch": "Ouvrir la recherche", + "closeSearch": "Fermer la recherche", + "myAccount": "Mon compte" + }, + "footer": { + "description": "Une boutique en ligne moderne propulsee par Spree Commerce et Next.js.", + "shop": "Boutique", + "allProducts": "Tous les produits", + "categories": "Categories", + "account": "Compte", + "myAccount": "Mon compte", + "orderHistory": "Historique des commandes", + "cart": "Panier", + "policies": "Politiques", + "poweredBy": "Propulse par", + "copyright": "\u00a9 {year} {storeName}. Propulse par Spree Commerce." + }, + "home": { + "welcome": "Bienvenue chez {storeName}", + "heroDescription": "Decouvrez des produits exceptionnels avec notre experience e-commerce moderne propulsee par Spree Commerce.", + "shopNow": "Acheter maintenant", + "browseCategories": "Parcourir les categories", + "featuredProducts": "Produits en vedette", + "viewAll": "Voir tout", + "qualityProducts": "Produits de qualite", + "qualityDescription": "Une selection soigneusement curee des meilleurs produits.", + "fastShipping": "Livraison rapide", + "shippingDescription": "Livraison rapide et fiable jusqu'a votre porte.", + "support": "Support 24/7", + "supportDescription": "Notre equipe est la pour vous aider a tout moment." + }, + "cart": { + "cart": "Panier", + "closeCart": "Fermer le panier", + "emptyCart": "Votre panier est vide", + "emptyCartDescription": "Il semble que vous n'ayez encore rien ajoute a votre panier.", + "removeItem": "Supprimer l'article", + "decreaseQuantity": "Diminuer la quantite", + "increaseQuantity": "Augmenter la quantite", + "shippingNote": "Livraison et taxes calculees lors du paiement", + "checkout": "Passer la commande", + "viewCart": "Voir le panier", + "shoppingCart": "Panier d'achat", + "proceedToCheckout": "Passer au paiement", + "itemCount": "({count, plural, one {# article} other {# articles}})", + "shippingCalculatedAtCheckout": "Calcule lors du paiement", + "removeItemLabel": "Supprimer {name}", + "giftCard": "Carte cadeau", + "storeCredit": "Credit boutique", + "amountDue": "Montant du" + }, + "products": { + "allProducts": "Tous les produits", + "browseCollection": "Parcourez notre collection complete", + "searchResultsFor": "Resultats de recherche pour \"{query}\"", + "productsFound": "{count, plural, one {# produit} other {# produits}} trouve(s)", + "noProductsFound": "Aucun produit trouve", + "tryAdjustingFilters": "Essayez d'ajuster vos filtres", + "noMatchingProducts": "Nous n'avons trouve aucun produit correspondant a \"{query}\"", + "showingProducts": "Affichage de {shown} sur {total} produits", + "loadingMore": "Chargement...", + "noMoreProducts": "Plus de produits a charger", + "filters": "Filtres", + "sortBy": "Trier par", + "resetFilters": "Reinitialiser les filtres", + "price": "Prix", + "availability": "Disponibilite", + "inStock": "En stock", + "outOfStock": "Rupture de stock", + "min": "Min", + "max": "Max", + "range": "Fourchette : {min} - {max}", + "clear": "Effacer", + "productCount": "{count, plural, one {# produit} other {# produits}}", + "sale": "Promo", + "outOfStockVariant": "(Rupture de stock)", + "addToCart": "Ajouter au panier", + "adding": "Ajout...", + "description": "Description", + "details": "Details", + "sku": "UGS", + "options": "Options", + "search": "Rechercher...", + "searching": "Recherche...", + "viewAllResultsFor": "Voir tous les resultats pour \"{query}\"", + "manual": "Manuel", + "bestSelling": "Meilleures ventes", + "priceLowHigh": "Prix (croissant)", + "priceHighLow": "Prix (decroissant)", + "newest": "Plus recents", + "oldest": "Plus anciens", + "nameAZ": "Nom (A-Z)", + "nameZA": "Nom (Z-A)", + "sortUnknown": "Autre", + "categories": "Categories", + "noCategoriesFound": "Aucune categorie trouvee.", + "noCategoriesInGroup": "Aucune categorie dans ce groupe.", + "subcategories": "{count, plural, one {# sous-categorie} other {# sous-categories}}", + "noImageAvailable": "Aucune image disponible", + "clickToZoom": "Cliquez pour agrandir", + "noProductsMatchingFilters": "Aucun produit ne correspond a vos filtres.", + "decreaseQuantity": "Diminuer la quantite", + "increaseQuantity": "Augmenter la quantite", + "carouselPrev": "Produits precedents", + "carouselNext": "Produits suivants", + "lightboxClose": "Fermer la visionneuse", + "lightboxPrev": "Image precedente", + "lightboxNext": "Image suivante", + "subcategoriesHeading": "Sous-categories", + "sort": "Trier", + "clearAll": "Tout effacer", + "clearAllFiltersCount": "Effacer tous les filtres ({count})", + "showResults": "Afficher les resultats", + "closeFilters": "Fermer les filtres", + "anyAvailability": "Toute disponibilite", + "customPrice": "Personnalise", + "priceLabel": "Prix : {value}", + "clearFilter": "Effacer {label}", + "openImageZoom": "Ouvrir le zoom de l'image", + "priceUnder": "Moins de {price}", + "priceAbove": "{price}+", + "priceRangeBucket": "{min} \u2013 {max}", + "properties": "Proprietes", + "yes": "Oui", + "no": "Non" + }, + "navigation": { + "home": "Accueil", + "categories": "Categories", + "breadcrumb": "Fil d'Ariane" + }, + "checkout": { + "checkout": "Paiement", + "orderNumber": "Commande #{number}", + "checkoutError": "Erreur de paiement", + "returnToCart": "Retourner au panier", + "emptyCart": "Votre panier est vide", + "emptyCartDescription": "Ajoutez des articles a votre panier avant de passer commande.", + "stepShipping": "Expedition", + "stepDelivery": "Livraison", + "stepPayment": "Paiement", + "contactInformation": "Coordonnees", + "emailAddress": "Adresse e-mail", + "emailPlaceholder": "vous@exemple.com", + "usingAccountEmail": "Utilisation de l'adresse e-mail de votre compte", + "signInPrompt": "Vous avez deja un compte ?", + "signIn": "Se connecter", + "signInDescription": "pour acceder a vos adresses enregistrees et a votre historique de commandes.", + "shippingAddress": "Adresse de livraison", + "continueToDelivery": "Continuer vers la livraison", + "shippingMethod": "Mode de livraison", + "loadingShippingOptions": "Chargement des options de livraison...", + "enterShippingAddressForMethods": "Entrez votre adresse de livraison pour voir les modes de livraison disponibles.", + "shipmentOf": "Expedition {current} sur {total}", + "shipsFrom": "Expedie depuis {location}", + "phone": "Telephone : {phone}", + "billingAddress": "Adresse de facturation", + "sameAsShipping": "Identique a l'adresse de livraison", + "paymentMethod": "Mode de paiement", + "loadingPaymentForm": "Chargement du formulaire de paiement...", + "addNewPaymentMethod": "Ajouter un nouveau mode de paiement", + "endingIn": "se terminant par", + "exp": "Exp.", + "savedCardLabel": "{brand} se terminant par {digits}", + "cardExpiry": "Exp. {month}/{year}", + "default": "Par defaut", + "noPaymentMethods": "Aucun mode de paiement disponible pour cette commande.", + "secureTransactions": "Toutes les transactions sont securisees et chiffrees.", + "creditCard": "Carte de credit", + "enterShippingAddress": "Entrer l'adresse de livraison", + "payNow": "Payer maintenant", + "failedToInitPayment": "Echec de l'initialisation du paiement. Veuillez reessayer.", + "failedToCreateSession": "Echec de la creation de la session de paiement.", + "paymentError": "Une erreur est survenue lors du paiement. Veuillez reessayer.", + "orderNotFound": "Commande introuvable ou vous n'y avez pas acces.", + "failedToLoadCheckout": "Echec du chargement du paiement. Veuillez reessayer.", + "orderNotFoundAfterPayment": "Commande introuvable apres le paiement. Veuillez contacter le support.", + "failedToSaveAddress": "Echec de l'enregistrement de l'adresse", + "failedToProceed": "Echec du passage a l'etape suivante", + "generalError": "Une erreur est survenue. Veuillez reessayer.", + "failedToSelectRate": "Echec de la selection du tarif de livraison", + "failedToSaveBilling": "Echec de l'enregistrement de l'adresse de facturation", + "failedToSaveBillingRetry": "Echec de l'enregistrement de l'adresse de facturation. Veuillez reessayer.", + "failedToCompleteOrder": "Echec de la finalisation de la commande", + "stripeNotLoaded": "Stripe n'est pas encore charge", + "paymentProcessingError": "Une erreur est survenue lors du paiement.", + "failedToCompletePaymentSession": "Echec de la finalisation de la session de paiement", + "noOrder": "Aucune commande", + "confirmingPayment": "Confirmation de votre paiement...", + "policyConsentRequired": "Vous devez accepter les politiques du magasin avant de passer votre commande", + "iAgreeToThe": "J'accepte les", + "policySeparatorComma": ", ", + "policySeparatorAnd": "et" + }, + "address": { + "firstName": "Prenom", + "lastName": "Nom", + "company": "Entreprise (facultatif)", + "address": "Adresse", + "streetAddress": "Adresse", + "apartment": "Appartement, suite, etc. (facultatif)", + "city": "Ville", + "country": "Pays", + "selectCountry": "Choisir un pays", + "stateProvince": "Etat / Province", + "selectState": "Choisir un etat", + "stateOrProvince": "Etat ou province", + "zipCode": "Code postal", + "phone": "Telephone (facultatif)", + "selectAnAddress": "Choisir une adresse", + "useDifferentAddress": "Utiliser une adresse differente", + "editAddress": "Modifier l'adresse", + "addNewAddress": "Ajouter une nouvelle adresse", + "saveAddress": "Enregistrer l'adresse", + "failedToSave": "Echec de l'enregistrement de l'adresse", + "deleteAddressTitle": "Supprimer l'adresse ?", + "deleteAddressConfirmation": "Cela supprimera definitivement cette adresse. Cette action est irreversible.", + "delete": "Supprimer", + "deleting": "Suppression...", + "cancel": "Annuler", + "edit": "Modifier", + "failedToDeleteAddress": "Echec de la suppression de l'adresse", + "addFirstAddress": "Ajoutez votre premiere adresse", + "addAddress": "Ajouter une adresse" + }, + "account": { + "myAccount": "Mon compte", + "signInDescription": "Connectez-vous pour acceder a votre compte et a votre historique de commandes.", + "email": "E-mail", + "password": "Mot de passe", + "signIn": "Se connecter", + "signingIn": "Connexion...", + "invalidCredentials": "E-mail ou mot de passe invalide", + "dontHaveAccount": "Vous n'avez pas de compte ?", + "signUp": "S'inscrire", + "accountOverview": "Apercu du compte", + "orderHistory": "Historique des commandes", + "orderHistoryDescription": "Consultez vos commandes passees et suivez vos expeditions", + "addresses": "Adresses", + "addressesDescription": "Gerez vos adresses de livraison et de facturation", + "paymentMethods": "Modes de paiement", + "paymentMethodsDescription": "Gerez vos cartes de credit enregistrees", + "profile": "Profil", + "profileDescription": "Mettez a jour vos informations personnelles", + "signOut": "Se deconnecter", + "overview": "Apercu", + "orders": "Commandes", + "giftCards": "Cartes cadeaux", + "addNewAddress": "Ajouter une nouvelle adresse", + "noAddresses": "Aucune adresse enregistree", + "noAddressesDescription": "Les adresses que vous ajoutez lors du paiement apparaitront ici.", + "defaultAddress": "Par defaut", + "deleteConfirm": "Etes-vous sur de vouloir supprimer cette adresse ?", + "setAsDefault": "Definir par defaut", + "showPassword": "Afficher le mot de passe", + "hidePassword": "Masquer le mot de passe", + "forgotPassword": "Mot de passe oublie ?", + "policyConsentRequired": "Vous devez accepter les politiques du magasin pour creer un compte" + }, + "register": { + "createAccount": "Creer un compte", + "signUpDescription": "Inscrivez-vous pour commencer a faire vos achats chez nous.", + "confirmPassword": "Confirmer le mot de passe", + "creatingAccount": "Creation du compte...", + "passwordsDontMatch": "Les mots de passe ne correspondent pas", + "passwordTooShort": "Le mot de passe doit contenir au moins 6 caracteres", + "registrationFailed": "Echec de l'inscription. Veuillez reessayer.", + "alreadyHaveAccount": "Vous avez deja un compte ?", + "signIn": "Se connecter", + "unexpectedError": "Une erreur inattendue est survenue. Veuillez reessayer.", + "firstName": "Prenom", + "lastName": "Nom", + "firstNamePlaceholder": "Jean", + "lastNamePlaceholder": "Dupont", + "emailPlaceholder": "vous@exemple.com" + }, + "orders": { + "orderHistory": "Historique des commandes", + "noOrders": "Aucune commande", + "noOrdersDescription": "Lorsque vous passerez des commandes, elles apparaitront ici.", + "startShopping": "Commencer les achats", + "order": "Commande", + "date": "Date", + "status": "Statut", + "actions": "Actions", + "view": "Voir", + "orderNotFound": "Commande introuvable", + "backToOrders": "Retour aux commandes", + "placedOn": "Passee le {date}", + "orderItems": "Articles de la commande", + "qty": "Qte : {quantity}", + "orderAgain": "Commander a nouveau", + "deliveryAddress": "Adresse de livraison", + "shippingMethod": "Mode de livraison", + "shippedFrom": "Expedie depuis {location}", + "trackItems": "Suivre les articles", + "shipmentCanceled": "Cette expedition a ete annulee.", + "noTrackingInfo": "Aucune information de suivi disponible.", + "paymentInformation": "Informations de paiement", + "cardEndingIn": "{label} se terminant par {digits}", + "cardExpires": "Expire le {month}/{year}", + "storeCreditApplied": "{amount} applique \u2014 {remaining} restant", + "billingAddress": "Adresse de facturation", + "orderNotFoundDescription": "La commande que vous recherchez n'existe pas.", + "storeCredit": "Credit boutique", + "specialInstructions": "Instructions speciales", + "payment": "Paiement", + "shipment": "Expedition", + "shipped": "Expedie", + "partiallyShipped": "Partiellement expedie", + "delivered": "Livre", + "ready": "Pret", + "pending": "En attente", + "canceled": "Annule", + "backorder": "En commande", + "paid": "Paye", + "balanceDue": "Solde du", + "creditOwed": "Credit du", + "failed": "Echoue", + "void": "Annule", + "notAvailable": "N/D", + "unknownShipmentStatus": "Inconnu", + "orderTitle": "Commande #{number}", + "shipmentCanceledRefund": "Expedition annulee \u2014 un remboursement a ete emis.", + "shippingMethodUnavailable": "Indisponible", + "totalColumn": "Total" + }, + "orderPlaced": { + "thanksForOrder": "Merci pour votre commande, {name} !", + "thanksForOrderAnonymous": "Merci pour votre commande !", + "orderNumber": "Commande #{number}", + "emailConfirmation": "Vous recevrez une confirmation par e-mail sous peu.", + "orderItems": "Articles de la commande", + "contactInformation": "Coordonnees", + "shippingAddress": "Adresse de livraison", + "billingAddress": "Adresse de facturation", + "orderNotFound": "Commande introuvable", + "failedToLoad": "Echec du chargement des details de la commande.", + "qty": "Qte : {quantity}", + "shippingMethod": "Mode de livraison", + "standardShipping": "Livraison standard", + "payment": "Paiement", + "giftCard": "Carte cadeau", + "confirmationSentTo": "Confirmation envoyee a" + }, + "coupon": { + "placeholder": "Carte cadeau ou code de reduction", + "apply": "Appliquer", + "remove": "Supprimer", + "invalidCode": "Code de coupon invalide", + "applyFailed": "Impossible d'appliquer le coupon. Veuillez reessayer.", + "failedToRemove": "Echec de la suppression du code de coupon", + "failedToRemoveGiftCard": "Echec de la suppression de la carte cadeau", + "removeCoupon": "Supprimer le coupon {code}", + "removeGiftCard": "Supprimer la carte cadeau", + "giftCardCode": "Carte cadeau {code}", + "applying": "Application...", + "removing": "Suppression..." + }, + "checkoutLayout": { + "backToStore": "Retour a la boutique", + "showOrderSummary": "Afficher le recapitulatif de la commande", + "hideOrderSummary": "Masquer le recapitulatif de la commande", + "allRightsReserved": "\u00a9 {year} {storeName}. Tous droits reserves." + }, + "profile": { + "profile": "Profil", + "firstName": "Prenom", + "lastName": "Nom", + "emailAddress": "Adresse e-mail", + "saveChanges": "Enregistrer les modifications", + "saving": "Enregistrement...", + "profileUpdated": "Profil mis a jour avec succes !", + "accountInformation": "Informations du compte", + "accountId": "ID du compte", + "loadingProfile": "Chargement du profil...", + "failedToUpdate": "Echec de la mise a jour du profil", + "email": "E-mail" + }, + "creditCards": { + "paymentMethods": "Modes de paiement", + "noCards": "Aucun mode de paiement enregistre", + "noCardsDescription": "Les modes de paiement sont enregistres automatiquement lorsque vous effectuez un achat.", + "expires": "Expire", + "default": "Par defaut", + "removing": "Suppression...", + "secureInfo": "Vos informations de paiement sont stockees en toute securite.", + "deleteConfirm": "Etes-vous sur de vouloir supprimer cette carte ?", + "cardMaskedLabel": "{label} se terminant par {digits}, expire {month}/{year}", + "removePaymentMethodTitle": "Supprimer le mode de paiement ?", + "endingIn": "se terminant par", + "cardEndingIn": "{label} se terminant par {digits}", + "cardExpires": "Expire {month}/{year}", + "remove": "Supprimer", + "cancel": "Annuler", + "confirmRemoveDescription": "Cela supprimera la {label} se terminant par {digits} de votre compte. Cette action est irreversible.", + "failedToRemove": "Echec de la suppression de la carte : {error}" + }, + "giftCards": { + "giftCards": "Cartes cadeaux", + "noGiftCards": "Aucune carte cadeau", + "noGiftCardsDescription": "Vous n'avez pas encore de cartes cadeaux.", + "active": "Active", + "partiallyUsed": "Partiellement utilisee", + "fullyRedeemed": "Entierement utilisee", + "canceled": "Annulee", + "expired": "Expiree", + "noExpiration": "Sans date d'expiration", + "remaining": "restant", + "usedAmount": "Utilise : {amount}", + "totalAmountWithValue": "Total : {amount}", + "addedOnDate": "Ajoutee le {date}", + "fullyRedeemedOnDate": "Entierement utilisee le {date}", + "activeGiftCards": "Cartes cadeaux actives", + "expiredRedeemed": "Expirees / Utilisees", + "copy": "Copier", + "copied": "Copie !", + "copyCodeToClipboard": "Copier le code dans le presse-papiers", + "expiresOn": "Expire le {date}", + "percentUsed": "{percent}% utilise", + "activeGiftCardsCount": "Cartes cadeaux actives ({count})", + "expiredRedeemedCount": "Expirees / Utilisees ({count})", + "giftCardsHelpText": "Les cartes cadeaux peuvent etre utilisees lors du paiement pour regler vos commandes. Le solde restant sera conserve pour de futurs achats." + }, + "forgotPassword": { + "title": "Reinitialiser votre mot de passe", + "description": "Entrez votre e-mail et nous vous enverrons un lien pour reinitialiser votre mot de passe.", + "email": "E-mail", + "sending": "Envoi...", + "sendResetLink": "Envoyer le lien de reinitialisation", + "backToSignIn": "Retour a la connexion", + "checkYourEmail": "Consultez votre e-mail", + "resetEmailSent": "Si un compte existe pour {email}, nous avons envoye les instructions de reinitialisation du mot de passe.", + "linkExpiry": "Le lien dans l'e-mail expirera dans 15 minutes. Verifiez votre dossier spam si vous ne le trouvez pas.", + "tryDifferentEmail": "Essayer un autre e-mail", + "genericError": "Une erreur est survenue. Veuillez reessayer." + }, + "resetPassword": { + "title": "Definir un nouveau mot de passe", + "description": "Entrez votre nouveau mot de passe ci-dessous.", + "newPassword": "Nouveau mot de passe", + "confirmPassword": "Confirmer le nouveau mot de passe", + "resetting": "Reinitialisation...", + "resetPassword": "Reinitialiser le mot de passe", + "backToSignIn": "Retour a la connexion", + "invalidLink": "Lien invalide", + "invalidLinkDescription": "Ce lien de reinitialisation de mot de passe est invalide ou a expire.", + "requestNewLink": "Demander un nouveau lien de reinitialisation", + "passwordsDontMatch": "Les mots de passe ne correspondent pas", + "passwordTooShort": "Le mot de passe doit contenir au moins 6 caracteres", + "linkExpired": "Ce lien de reinitialisation a expire. Veuillez en demander un nouveau.", + "genericError": "Une erreur est survenue. Veuillez reessayer.", + "success": "Mot de passe reinitialise avec succes", + "successDescription": "Votre mot de passe a ete mis a jour. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.", + "signIn": "Se connecter" + }, + "policies": { + "policyNotFound": "Politique introuvable", + "noContent": "Aucun contenu disponible pour cette politique." + } +} diff --git a/src/app/[country]/[locale]/layout.tsx b/src/app/[country]/[locale]/layout.tsx index ff12c3d2..7763cafc 100644 --- a/src/app/[country]/[locale]/layout.tsx +++ b/src/app/[country]/[locale]/layout.tsx @@ -9,6 +9,8 @@ import { generateStoreMetadata } from "@/lib/metadata/store"; import { buildOrganizationJsonLd } from "@/lib/seo"; import deMessages from "../../../../messages/de.json"; import enMessages from "../../../../messages/en.json"; +import esMessages from "../../../../messages/es.json"; +import frMessages from "../../../../messages/fr.json"; import plMessages from "../../../../messages/pl.json"; const DEFAULT_COUNTRY = process.env.NEXT_PUBLIC_DEFAULT_COUNTRY || "us"; @@ -17,6 +19,8 @@ const DEFAULT_LOCALE = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || "en"; const messagesMap: Record = { en: enMessages, de: deMessages, + es: esMessages, + fr: frMessages, pl: plMessages, }; diff --git a/src/i18n/request.ts b/src/i18n/request.ts index f84b5eaa..5545d33f 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -1,7 +1,7 @@ import type { Locale } from "next-intl"; import { getRequestConfig } from "next-intl/server"; -const supportedLocales: Locale[] = ["en", "de", "pl"]; +const supportedLocales: Locale[] = ["en", "de", "pl", "es", "fr"]; function isValidLocale(value: string | undefined): value is Locale { return !!value && supportedLocales.includes(value as Locale); diff --git a/src/types/next-intl.d.ts b/src/types/next-intl.d.ts index b45f7f87..38aeb05d 100644 --- a/src/types/next-intl.d.ts +++ b/src/types/next-intl.d.ts @@ -3,7 +3,7 @@ import type messages from "../../messages/en.json"; type Messages = typeof messages; declare global { - type Locale = "en" | "de" | "pl"; + type Locale = "en" | "de" | "pl" | "es" | "fr"; interface IntlMessages extends Messages {} } From 055b279b200869cbf2e80eb6466fd11e651427cd Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 20:43:28 +0200 Subject: [PATCH 22/28] Address code review findings: sanitize errors, localize statuses, server components Inline fixes: - CreditCardList: remove raw SDK error from alert, use generic message - AddressManagement: replace string concatenation with clean translated message - OrderList: localize payment/fulfillment status badges via translation keys, convert to async server component (no client hooks needed) - PolicyConsent + policies.ts: add nameKey to policy entries, render translated policy names in consent checkbox, footer, and checkout layout - CouponCode: wrap all async handlers in try/catch/finally to prevent stuck state Additional improvements: - Breadcrumbs: convert to async server component with locale prop - FilterChips: replace hardcoded colon-separated label with translation key Co-Authored-By: Claude Opus 4.6 (1M context) --- messages/de.json | 13 ++++-- messages/en.json | 13 ++++-- messages/es.json | 13 ++++-- messages/fr.json | 13 ++++-- messages/pl.json | 13 ++++-- .../[country]/[locale]/(checkout)/layout.tsx | 3 +- .../(storefront)/account/orders/page.tsx | 2 +- .../(storefront)/c/[...permalink]/page.tsx | 6 ++- .../(storefront)/products/[slug]/page.tsx | 1 + src/components/account/CreditCardList.tsx | 2 +- src/components/account/OrderList.tsx | 32 +++++++++---- .../addresses/AddressManagement.tsx | 2 +- src/components/checkout/CouponCode.tsx | 46 ++++++++++++------- src/components/layout/Footer.tsx | 3 +- src/components/navigation/Breadcrumbs.tsx | 13 ++++-- src/components/policy/PolicyConsent.tsx | 3 +- .../products/filters/FilterChips.tsx | 5 +- src/lib/constants/policies.ts | 24 +++++++--- 18 files changed, 142 insertions(+), 65 deletions(-) diff --git a/messages/de.json b/messages/de.json index cd497379..5ac66e3f 100644 --- a/messages/de.json +++ b/messages/de.json @@ -159,7 +159,8 @@ "priceRangeBucket": "{min} – {max}", "properties": "Eigenschaften", "yes": "Ja", - "no": "Nein" + "no": "Nein", + "optionFilterChip": "{filter}: {value}" }, "navigation": { "home": "Startseite", @@ -256,7 +257,7 @@ "deleting": "Wird gelöscht...", "cancel": "Abbrechen", "edit": "Bearbeiten", - "failedToDeleteAddress": "Adresse konnte nicht gelöscht werden", + "failedToDeleteAddress": "Adresse konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", "addFirstAddress": "Erste Adresse hinzufügen", "addAddress": "Adresse hinzufügen" }, @@ -431,7 +432,7 @@ "remove": "Entfernen", "cancel": "Abbrechen", "confirmRemoveDescription": "Die {label} mit Endung {digits} wird von Ihrem Konto entfernt. Diese Aktion kann nicht rückgängig gemacht werden.", - "failedToRemove": "Karte konnte nicht entfernt werden: {error}" + "failedToRemove": "Karte konnte nicht entfernt werden. Bitte versuchen Sie es erneut." }, "giftCards": { "giftCards": "Geschenkkarten", @@ -493,6 +494,10 @@ }, "policies": { "policyNotFound": "Richtlinie nicht gefunden", - "noContent": "Für diese Richtlinie sind keine Inhalte verfügbar." + "noContent": "Für diese Richtlinie sind keine Inhalte verfügbar.", + "shippingPolicy": "Versandrichtlinie", + "privacyPolicy": "Datenschutzrichtlinie", + "returnsPolicy": "Rückgaberichtlinie", + "termsOfService": "Nutzungsbedingungen" } } diff --git a/messages/en.json b/messages/en.json index 461c4407..3d8e5784 100644 --- a/messages/en.json +++ b/messages/en.json @@ -159,7 +159,8 @@ "priceRangeBucket": "{min} – {max}", "properties": "Properties", "yes": "Yes", - "no": "No" + "no": "No", + "optionFilterChip": "{filter}: {value}" }, "navigation": { "home": "Home", @@ -256,7 +257,7 @@ "deleting": "Deleting...", "cancel": "Cancel", "edit": "Edit", - "failedToDeleteAddress": "Failed to delete address", + "failedToDeleteAddress": "Failed to delete address. Please try again.", "addFirstAddress": "Add Your First Address", "addAddress": "Add Address" }, @@ -431,7 +432,7 @@ "remove": "Remove", "cancel": "Cancel", "confirmRemoveDescription": "This will remove the {label} ending in {digits} from your account. This action cannot be undone.", - "failedToRemove": "Failed to remove card: {error}" + "failedToRemove": "Failed to remove card. Please try again." }, "giftCards": { "giftCards": "Gift Cards", @@ -493,6 +494,10 @@ }, "policies": { "policyNotFound": "Policy Not Found", - "noContent": "No content available for this policy." + "noContent": "No content available for this policy.", + "shippingPolicy": "Shipping Policy", + "privacyPolicy": "Privacy Policy", + "returnsPolicy": "Returns Policy", + "termsOfService": "Terms of Service" } } diff --git a/messages/es.json b/messages/es.json index 9d0c1b71..585e5ae8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -159,7 +159,8 @@ "priceRangeBucket": "{min} \u2013 {max}", "properties": "Propiedades", "yes": "Si", - "no": "No" + "no": "No", + "optionFilterChip": "{filter}: {value}" }, "navigation": { "home": "Inicio", @@ -256,7 +257,7 @@ "deleting": "Eliminando...", "cancel": "Cancelar", "edit": "Editar", - "failedToDeleteAddress": "Error al eliminar la direccion", + "failedToDeleteAddress": "Error al eliminar la direccion. Por favor, intenta de nuevo.", "addFirstAddress": "Agrega tu primera direccion", "addAddress": "Agregar direccion" }, @@ -431,7 +432,7 @@ "remove": "Eliminar", "cancel": "Cancelar", "confirmRemoveDescription": "Esto eliminara la {label} terminada en {digits} de tu cuenta. Esta accion no se puede deshacer.", - "failedToRemove": "Error al eliminar la tarjeta: {error}" + "failedToRemove": "Error al eliminar la tarjeta. Por favor, intenta de nuevo." }, "giftCards": { "giftCards": "Tarjetas regalo", @@ -493,6 +494,10 @@ }, "policies": { "policyNotFound": "Politica no encontrada", - "noContent": "No hay contenido disponible para esta politica." + "noContent": "No hay contenido disponible para esta politica.", + "shippingPolicy": "Politica de envio", + "privacyPolicy": "Politica de privacidad", + "returnsPolicy": "Politica de devoluciones", + "termsOfService": "Terminos de servicio" } } diff --git a/messages/fr.json b/messages/fr.json index 4c848b03..47f60b41 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -159,7 +159,8 @@ "priceRangeBucket": "{min} \u2013 {max}", "properties": "Proprietes", "yes": "Oui", - "no": "Non" + "no": "Non", + "optionFilterChip": "{filter} : {value}" }, "navigation": { "home": "Accueil", @@ -256,7 +257,7 @@ "deleting": "Suppression...", "cancel": "Annuler", "edit": "Modifier", - "failedToDeleteAddress": "Echec de la suppression de l'adresse", + "failedToDeleteAddress": "Echec de la suppression de l'adresse. Veuillez reessayer.", "addFirstAddress": "Ajoutez votre premiere adresse", "addAddress": "Ajouter une adresse" }, @@ -431,7 +432,7 @@ "remove": "Supprimer", "cancel": "Annuler", "confirmRemoveDescription": "Cela supprimera la {label} se terminant par {digits} de votre compte. Cette action est irreversible.", - "failedToRemove": "Echec de la suppression de la carte : {error}" + "failedToRemove": "Echec de la suppression de la carte. Veuillez reessayer." }, "giftCards": { "giftCards": "Cartes cadeaux", @@ -493,6 +494,10 @@ }, "policies": { "policyNotFound": "Politique introuvable", - "noContent": "Aucun contenu disponible pour cette politique." + "noContent": "Aucun contenu disponible pour cette politique.", + "shippingPolicy": "Politique de livraison", + "privacyPolicy": "Politique de confidentialite", + "returnsPolicy": "Politique de retour", + "termsOfService": "Conditions generales" } } diff --git a/messages/pl.json b/messages/pl.json index 9a3ed9ee..143304f3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -159,7 +159,8 @@ "priceRangeBucket": "{min} – {max}", "properties": "Właściwości", "yes": "Tak", - "no": "Nie" + "no": "Nie", + "optionFilterChip": "{filter}: {value}" }, "navigation": { "home": "Strona główna", @@ -256,7 +257,7 @@ "deleting": "Usuwanie...", "cancel": "Anuluj", "edit": "Edytuj", - "failedToDeleteAddress": "Nie udało się usunąć adresu", + "failedToDeleteAddress": "Nie udało się usunąć adresu. Spróbuj ponownie.", "addFirstAddress": "Dodaj swój pierwszy adres", "addAddress": "Dodaj adres" }, @@ -431,7 +432,7 @@ "remove": "Usuń", "cancel": "Anuluj", "confirmRemoveDescription": "Karta {label} kończąca się na {digits} zostanie usunięta z Twojego konta. Tej operacji nie można cofnąć.", - "failedToRemove": "Nie udało się usunąć karty: {error}" + "failedToRemove": "Nie udało się usunąć karty. Spróbuj ponownie." }, "giftCards": { "giftCards": "Karty podarunkowe", @@ -493,6 +494,10 @@ }, "policies": { "policyNotFound": "Nie znaleziono regulaminu", - "noContent": "Brak treści dla tego regulaminu." + "noContent": "Brak treści dla tego regulaminu.", + "shippingPolicy": "Polityka wysyłki", + "privacyPolicy": "Polityka prywatności", + "returnsPolicy": "Polityka zwrotów", + "termsOfService": "Regulamin" } } diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index faeb98bb..860dd624 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -45,6 +45,7 @@ function CheckoutFooter() { const pathname = usePathname(); const basePath = extractBasePath(pathname); const t = useTranslations("checkoutLayout"); + const tp = useTranslations("policies"); return (
    @@ -58,7 +59,7 @@ function CheckoutFooter() { target="_blank" className="text-gray-500 underline hover:text-gray-700" > - {policy.name} + {tp(policy.nameKey)} ))}
    diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx index c4c962b9..d89bb56c 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -40,7 +40,7 @@ export default async function OrdersPage({ params }: OrdersPageProps) {
    ) : ( - + )}
    ); diff --git a/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx b/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx index 0fd9679f..c7528990 100644 --- a/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx @@ -55,7 +55,11 @@ export default async function CategoryPage({ params }: CategoryPageProps) { style={{ backgroundImage: `url(${category.image_url})` }} >
    - +

    diff --git a/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx b/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx index a7ae860b..79874581 100644 --- a/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx @@ -90,6 +90,7 @@ export default async function ProductPage({ category={breadcrumbCategory} basePath={basePath} productName={product.name} + locale={locale} /> )}

    diff --git a/src/components/account/CreditCardList.tsx b/src/components/account/CreditCardList.tsx index 110557c7..62095f0d 100644 --- a/src/components/account/CreditCardList.tsx +++ b/src/components/account/CreditCardList.tsx @@ -116,7 +116,7 @@ export function CreditCardList({ initialCards }: CreditCardListProps) { if (result.success) { setCards((prev) => prev.filter((card) => card.id !== id)); } else { - alert(t("failedToRemove", { error: result.error })); + alert(t("failedToRemove")); } }; diff --git a/src/components/account/OrderList.tsx b/src/components/account/OrderList.tsx index 8ae31e67..9aab79a4 100644 --- a/src/components/account/OrderList.tsx +++ b/src/components/account/OrderList.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { Order } from "@spree/sdk"; import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { Button } from "@/components/ui/button"; import { formatDate, @@ -10,14 +8,31 @@ import { getPaymentStatusColor, } from "@/lib/utils/format"; +function getStatusLabel( + status: string | null, + t: (key: string) => string, +): string { + if (!status) return t("notAvailable"); + // Map API statuses like "balance_due" to translation keys like "balanceDue" + const key = status.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase()); + try { + return t(key); + } catch { + return status.replace(/_/g, " "); + } +} + interface OrderListProps { orders: Order[]; basePath: string; + locale: string; } -export function OrderList({ orders, basePath }: OrderListProps) { - const t = useTranslations("orders"); - const locale = useLocale(); +export async function OrderList({ orders, basePath, locale }: OrderListProps) { + const t = await getTranslations({ + locale: locale as Locale, + namespace: "orders", + }); return (
    @@ -66,15 +81,14 @@ export function OrderList({ orders, basePath }: OrderListProps) { - {order.payment_status?.replace("_", " ") || - t("notAvailable")} + {getStatusLabel(order.payment_status ?? null, t)} - {order.fulfillment_status || t("notAvailable")} + {getStatusLabel(order.fulfillment_status ?? null, t)} diff --git a/src/components/addresses/AddressManagement.tsx b/src/components/addresses/AddressManagement.tsx index 8f5ac334..16b06b78 100644 --- a/src/components/addresses/AddressManagement.tsx +++ b/src/components/addresses/AddressManagement.tsx @@ -157,7 +157,7 @@ export function AddressManagement({ if (result.success) { setAddresses((prev) => prev.filter((addr) => addr.id !== id)); } else { - alert(`${t("failedToDeleteAddress")}: ${result.error}`); + alert(t("failedToDeleteAddress")); } }; diff --git a/src/components/checkout/CouponCode.tsx b/src/components/checkout/CouponCode.tsx index cade14b6..1c27a22e 100644 --- a/src/components/checkout/CouponCode.tsx +++ b/src/components/checkout/CouponCode.tsx @@ -43,26 +43,34 @@ export function CouponCode({ setApplying(true); setError(null); - const result = await onApply(code.trim()); - if (result.success) { - setCode(""); - } else { - setError(result.error || t("invalidCode")); + try { + const result = await onApply(code.trim()); + if (result.success) { + setCode(""); + } else { + setError(result.error || t("invalidCode")); + } + } catch { + setError(t("invalidCode")); + } finally { + setApplying(false); } - - setApplying(false); }; const handleRemoveDiscount = async (discountCode: string) => { setRemoving(discountCode); setError(null); - const result = await onRemoveDiscount(discountCode); - if (!result.success) { - setError(result.error || t("failedToRemove")); + try { + const result = await onRemoveDiscount(discountCode); + if (!result.success) { + setError(result.error || t("failedToRemove")); + } + } catch { + setError(t("failedToRemove")); + } finally { + setRemoving(null); } - - setRemoving(null); }; const handleRemoveGiftCard = async () => { @@ -71,12 +79,16 @@ export function CouponCode({ setRemoving(appliedGiftCard.id); setError(null); - const result = await onRemoveGiftCard(appliedGiftCard.id); - if (!result.success) { - setError(result.error || t("failedToRemoveGiftCard")); + try { + const result = await onRemoveGiftCard(appliedGiftCard.id); + if (!result.success) { + setError(result.error || t("failedToRemoveGiftCard")); + } + } catch { + setError(t("failedToRemoveGiftCard")); + } finally { + setRemoving(null); } - - setRemoving(null); }; const hasAppliedCode = couponPromotions.length > 0 || !!appliedGiftCard; diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 2c9dbff8..ba5da152 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -20,6 +20,7 @@ export async function Footer({ locale, }: FooterProps) { const t = await getTranslations({ locale, namespace: "footer" }); + const tp = await getTranslations({ locale, namespace: "policies" }); return (
    @@ -103,7 +104,7 @@ export async function Footer({ href={`${basePath}/policies/${policy.slug}`} className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors" > - {policy.name} + {tp(policy.nameKey)} ))} diff --git a/src/components/navigation/Breadcrumbs.tsx b/src/components/navigation/Breadcrumbs.tsx index 9325802c..d8d3e90f 100644 --- a/src/components/navigation/Breadcrumbs.tsx +++ b/src/components/navigation/Breadcrumbs.tsx @@ -1,22 +1,25 @@ -"use client"; - import type { Category } from "@spree/sdk"; import { ChevronRight } from "lucide-react"; import Link from "next/link"; -import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; interface BreadcrumbsProps { category: Category; basePath: string; productName?: string; + locale: string; } -export function Breadcrumbs({ +export async function Breadcrumbs({ category, basePath, productName, + locale, }: BreadcrumbsProps) { - const t = useTranslations("navigation"); + const t = await getTranslations({ + locale: locale as Locale, + namespace: "navigation", + }); // Build breadcrumb items from ancestors + current category const items = [{ name: t("home"), href: basePath }]; diff --git a/src/components/policy/PolicyConsent.tsx b/src/components/policy/PolicyConsent.tsx index dbcf2af0..2cd6ce44 100644 --- a/src/components/policy/PolicyConsent.tsx +++ b/src/components/policy/PolicyConsent.tsx @@ -22,6 +22,7 @@ export function PolicyConsent({ const pathname = usePathname(); const basePath = extractBasePath(pathname); const t = useTranslations("checkout"); + const tp = useTranslations("policies"); return (
    - {policy.name} + {tp(policy.nameKey)} ))} diff --git a/src/components/products/filters/FilterChips.tsx b/src/components/products/filters/FilterChips.tsx index 4cfcb2c9..42680e85 100644 --- a/src/components/products/filters/FilterChips.tsx +++ b/src/components/products/filters/FilterChips.tsx @@ -46,7 +46,10 @@ export function FilterChips({ if (option) { chips.push({ key: `option-${optionValueId}`, - label: `${optionFilter.label}: ${option.label}`, + label: t("optionFilterChip", { + filter: optionFilter.label, + value: option.label, + }), onRemove: () => onRemoveOptionValue(optionValueId), }); } diff --git a/src/lib/constants/policies.ts b/src/lib/constants/policies.ts index fa10ca05..9fe0a97e 100644 --- a/src/lib/constants/policies.ts +++ b/src/lib/constants/policies.ts @@ -1,12 +1,24 @@ export const POLICY_LINKS = [ - { name: "Shipping Policy", slug: "shipping-policy" }, - { name: "Privacy Policy", slug: "privacy-policy" }, - { name: "Returns Policy", slug: "returns-policy" }, - { name: "Terms of Service", slug: "terms-of-service" }, + { + name: "Shipping Policy", + nameKey: "shippingPolicy", + slug: "shipping-policy", + }, + { name: "Privacy Policy", nameKey: "privacyPolicy", slug: "privacy-policy" }, + { name: "Returns Policy", nameKey: "returnsPolicy", slug: "returns-policy" }, + { + name: "Terms of Service", + nameKey: "termsOfService", + slug: "terms-of-service", + }, ]; /** Policies shown in the consent checkbox (checkout + registration). */ export const CONSENT_POLICIES = [ - { name: "Privacy Policy", slug: "privacy-policy" }, - { name: "Terms of Service", slug: "terms-of-service" }, + { name: "Privacy Policy", nameKey: "privacyPolicy", slug: "privacy-policy" }, + { + name: "Terms of Service", + nameKey: "termsOfService", + slug: "terms-of-service", + }, ]; From a9739a3ee6ffa7bda83d032ff626b0adac96149e Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 20:46:18 +0200 Subject: [PATCH 23/28] Address remaining nitpick findings from code review - Policy page: add description and openGraph metadata, explicit return type - Root layout: add explicit fallback={null} to Suspense boundary - PolicyConsent: move conjunction spacing into translation strings - ProductCustomFields: use namespace-specific ReturnType> Co-Authored-By: Claude Opus 4.6 (1M context) --- messages/de.json | 2 +- messages/en.json | 2 +- messages/es.json | 2 +- messages/fr.json | 2 +- messages/pl.json | 2 +- .../(storefront)/policies/[slug]/page.tsx | 18 ++++++++++++++---- src/app/layout.tsx | 2 +- src/components/policy/PolicyConsent.tsx | 2 +- .../products/ProductCustomFields.tsx | 4 ++-- 9 files changed, 23 insertions(+), 13 deletions(-) diff --git a/messages/de.json b/messages/de.json index 5ac66e3f..32c333c1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -228,7 +228,7 @@ "policyConsentRequired": "Sie müssen den Geschäftsrichtlinien zustimmen, bevor Sie Ihre Bestellung aufgeben", "iAgreeToThe": "Ich stimme zu", "policySeparatorComma": ", ", - "policySeparatorAnd": "und" + "policySeparatorAnd": " und " }, "address": { "firstName": "Vorname", diff --git a/messages/en.json b/messages/en.json index 3d8e5784..99182e4c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -228,7 +228,7 @@ "policyConsentRequired": "You must agree to the store policies before placing your order", "iAgreeToThe": "I agree to the", "policySeparatorComma": ", ", - "policySeparatorAnd": "and" + "policySeparatorAnd": " and " }, "address": { "firstName": "First name", diff --git a/messages/es.json b/messages/es.json index 585e5ae8..876b61f8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -228,7 +228,7 @@ "policyConsentRequired": "Debes aceptar las politicas de la tienda antes de realizar tu pedido", "iAgreeToThe": "Acepto las", "policySeparatorComma": ", ", - "policySeparatorAnd": "y" + "policySeparatorAnd": " y " }, "address": { "firstName": "Nombre", diff --git a/messages/fr.json b/messages/fr.json index 47f60b41..3d315c95 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -228,7 +228,7 @@ "policyConsentRequired": "Vous devez accepter les politiques du magasin avant de passer votre commande", "iAgreeToThe": "J'accepte les", "policySeparatorComma": ", ", - "policySeparatorAnd": "et" + "policySeparatorAnd": " et " }, "address": { "firstName": "Prenom", diff --git a/messages/pl.json b/messages/pl.json index 143304f3..4cdd449c 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -228,7 +228,7 @@ "policyConsentRequired": "Musisz zaakceptować regulamin sklepu przed złożeniem zamówienia", "iAgreeToThe": "Zgadzam się z", "policySeparatorComma": ", ", - "policySeparatorAnd": "i" + "policySeparatorAnd": " i " }, "address": { "firstName": "Imię", diff --git a/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx b/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx index 9b6dd26c..659eb9fc 100644 --- a/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx @@ -18,22 +18,32 @@ export async function generateMetadata({ const { slug, locale } = await params; const policy = await getPolicy(slug); + const storeName = getStoreName(); + if (!policy) { const t = await getTranslations({ locale: locale as Locale, namespace: "policies", }); - return { title: t("policyNotFound") }; + return { + title: t("policyNotFound"), + description: t("noContent"), + }; } - const storeName = getStoreName(); - return { title: storeName ? `${policy.name} | ${storeName}` : policy.name, + description: `${policy.name} — ${storeName}`, + openGraph: { + title: policy.name, + description: `${policy.name} — ${storeName}`, + }, }; } -export default async function PolicyPage({ params }: PolicyPageProps) { +export default async function PolicyPage({ + params, +}: PolicyPageProps): Promise { const { slug, locale } = await params; const [policy, t] = await Promise.all([ getPolicy(slug), diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 113cbacf..3dcdd6b8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,7 +37,7 @@ export default function RootLayout({ - + {children} diff --git a/src/components/policy/PolicyConsent.tsx b/src/components/policy/PolicyConsent.tsx index 2cd6ce44..9618133b 100644 --- a/src/components/policy/PolicyConsent.tsx +++ b/src/components/policy/PolicyConsent.tsx @@ -43,7 +43,7 @@ export function PolicyConsent({ t("policySeparatorComma")} {index > 0 && index === CONSENT_POLICIES.length - 1 && - ` ${t("policySeparatorAnd")} `} + t("policySeparatorAnd")} , + t: ReturnType>, ): React.ReactNode { return value ? t("yes") : t("no"); } @@ -19,7 +19,7 @@ function renderBooleanValue( function renderValue( field: CustomField, normalizedType: string, - t: ReturnType, + t: ReturnType>, ): React.ReactNode { switch (normalizedType) { case "Boolean": From 9b895147d442e393edbba1948b045adfabb59a3b Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 21:16:19 +0200 Subject: [PATCH 24/28] Fix remaining review findings: OrderDetail extraction, CouponCode logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrderDetail: extracted LineItemCard and FulfillmentBlock into separate client component files (src/components/order/), converted OrderDetail to async server component with getTranslations - CouponCode: changed hasAppliedCode logic — input now hides only when BOTH a coupon AND a gift card are applied (was hiding on either) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../(storefront)/account/orders/[id]/page.tsx | 2 +- src/components/account/OrderDetail.tsx | 171 ++---------------- src/components/checkout/CouponCode.tsx | 5 +- src/components/order/FulfillmentBlock.tsx | 103 +++++++++++ src/components/order/LineItemCard.tsx | 57 ++++++ 5 files changed, 180 insertions(+), 158 deletions(-) create mode 100644 src/components/order/FulfillmentBlock.tsx create mode 100644 src/components/order/LineItemCard.tsx diff --git a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx index 4f9b3550..72d24331 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/[id]/page.tsx @@ -41,5 +41,5 @@ export default async function OrderDetailPage({ ); } - return ; + return ; } diff --git a/src/components/account/OrderDetail.tsx b/src/components/account/OrderDetail.tsx index f534c3e4..9d4c8ede 100644 --- a/src/components/account/OrderDetail.tsx +++ b/src/components/account/OrderDetail.tsx @@ -1,168 +1,29 @@ -"use client"; - -import type { Address, Fulfillment, Order } from "@spree/sdk"; -import { ChevronLeft, CircleAlert } from "lucide-react"; +import type { Order } from "@spree/sdk"; +import { ChevronLeft } from "lucide-react"; import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { AddressBlock } from "@/components/order/AddressBlock"; +import { FulfillmentBlock } from "@/components/order/FulfillmentBlock"; +import { LineItemCard } from "@/components/order/LineItemCard"; import { OrderTotals } from "@/components/order/OrderTotals"; import { PaymentInfo } from "@/components/order/PaymentInfo"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { ProductImage } from "@/components/ui/product-image"; -import { formatDateTime, getFulfillmentStatusColor } from "@/lib/utils/format"; - -function LineItemCard({ - item, - basePath, -}: { - item: Order["items"][number]; - basePath: string; -}) { - const t = useTranslations("orders"); - return ( -
    - - - - -
    - - {item.name} - -
    {item.display_price}
    - {item.options_text && ( -

    {item.options_text}

    - )} -

    - {t("qty", { quantity: item.quantity })} -

    - - {t("orderAgain")} - -
    - -
    - {item.display_total} -
    -
    - ); -} - -function FulfillmentBlock({ - fulfillment, - shipAddress, - basePath, - lineItems, -}: { - fulfillment: Fulfillment; - shipAddress: Address | null; - basePath: string; - lineItems: Order["items"]; -}) { - const t = useTranslations("orders"); - return ( -
    -
    -
    - {shipAddress && ( -
    -

    - {t("deliveryAddress")} -

    - -
    - )} -
    -
    -

    - {t("shippingMethod")} -

    -

    - {fulfillment.delivery_method?.name || t("canceled")} -

    - {fulfillment.stock_location && ( -

    - {t("shippedFrom", { - location: fulfillment.stock_location.name, - })} -

    - )} - - {fulfillment.status} - -
    -
    - {fulfillment.status === "shipped" && fulfillment.tracking_url ? ( - - ) : ( - - )} -
    -
    -
    - - {fulfillment.status === "canceled" && !fulfillment.fulfilled_at && ( - - - {t("shipmentCanceledRefund")} - - )} - {fulfillment.status !== "canceled" && - fulfillment.status !== "shipped" && - !fulfillment.tracking && ( -
    - {t("noTrackingInfo")} -
    - )} -
    - -
    - {lineItems.map((item) => ( -
    - -
    - ))} -
    -
    - ); -} +import { formatDateTime } from "@/lib/utils/format"; interface OrderDetailProps { order: Order; basePath: string; + locale: string; } -export function OrderDetail({ order, basePath }: OrderDetailProps) { - const t = useTranslations("orders"); - const locale = useLocale(); +export async function OrderDetail({ + order, + basePath, + locale, +}: OrderDetailProps) { + const t = await getTranslations({ + locale: locale as Locale, + namespace: "orders", + }); const hasFulfillments = order.fulfillments && order.fulfillments.length > 0; return ( diff --git a/src/components/checkout/CouponCode.tsx b/src/components/checkout/CouponCode.tsx index 1c27a22e..ab90c1c7 100644 --- a/src/components/checkout/CouponCode.tsx +++ b/src/components/checkout/CouponCode.tsx @@ -91,7 +91,8 @@ export function CouponCode({ } }; - const hasAppliedCode = couponPromotions.length > 0 || !!appliedGiftCard; + // Hide the input only when both a coupon and a gift card are applied + const hasAllCodesApplied = couponPromotions.length > 0 && !!appliedGiftCard; return (
    @@ -153,7 +154,7 @@ export function CouponCode({ )} {/* Apply new code — single input for discount codes and gift cards */} - {!hasAppliedCode && ( + {!hasAllCodesApplied && (
    +
    +
    + {shipAddress && ( +
    +

    + {t("deliveryAddress")} +

    + +
    + )} +
    +
    +

    + {t("shippingMethod")} +

    +

    + {fulfillment.delivery_method?.name || t("canceled")} +

    + {fulfillment.stock_location && ( +

    + {t("shippedFrom", { + location: fulfillment.stock_location.name, + })} +

    + )} + + {fulfillment.status} + +
    +
    + {fulfillment.status === "shipped" && fulfillment.tracking_url ? ( + + ) : ( + + )} +
    +
    +
    + + {fulfillment.status === "canceled" && !fulfillment.fulfilled_at && ( + + + {t("shipmentCanceledRefund")} + + )} + {fulfillment.status !== "canceled" && + fulfillment.status !== "shipped" && + !fulfillment.tracking && ( +
    + {t("noTrackingInfo")} +
    + )} +
    + +
    + {lineItems.map((item) => ( +
    + +
    + ))} +
    +
    + ); +} diff --git a/src/components/order/LineItemCard.tsx b/src/components/order/LineItemCard.tsx new file mode 100644 index 00000000..bd773885 --- /dev/null +++ b/src/components/order/LineItemCard.tsx @@ -0,0 +1,57 @@ +"use client"; + +import type { Order } from "@spree/sdk"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { ProductImage } from "@/components/ui/product-image"; + +interface LineItemCardProps { + item: Order["items"][number]; + basePath: string; +} + +export function LineItemCard({ item, basePath }: LineItemCardProps) { + const t = useTranslations("orders"); + return ( +
    + + + + +
    + + {item.name} + +
    {item.display_price}
    + {item.options_text && ( +

    {item.options_text}

    + )} +

    + {t("qty", { quantity: item.quantity })} +

    + + {t("orderAgain")} + +
    + +
    + {item.display_total} +
    +
    + ); +} From 225e1f540fb229470dacc6f7ff92ee136fca96d1 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 21:39:34 +0200 Subject: [PATCH 25/28] Use translated footer description instead of hardcoded English fallback Footer now uses t("footer.description") from locale files when NEXT_PUBLIC_STORE_DESCRIPTION env var is not set. Removed unused storeDescription from StoreContext. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/layout/Footer.tsx | 8 ++++---- src/contexts/StoreContext.tsx | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index ba5da152..12f7be69 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -4,9 +4,7 @@ import { getTranslations } from "next-intl/server"; import { POLICY_LINKS } from "@/lib/constants/policies"; const storeName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; -const storeDescription = - process.env.NEXT_PUBLIC_STORE_DESCRIPTION || - "A modern e-commerce storefront powered by Spree Commerce and Next.js."; +const envStoreDescription = process.env.NEXT_PUBLIC_STORE_DESCRIPTION; interface FooterProps { rootCategories: Category[]; @@ -29,7 +27,9 @@ export async function Footer({ {/* Brand */}
    {storeName} -

    {storeDescription}

    +

    + {envStoreDescription || t("description")} +

    {/* Links */} diff --git a/src/contexts/StoreContext.tsx b/src/contexts/StoreContext.tsx index 7bc90c26..62597f01 100644 --- a/src/contexts/StoreContext.tsx +++ b/src/contexts/StoreContext.tsx @@ -12,13 +12,9 @@ export interface CountryWithMarket extends Country { } const storeName = getStoreName(); -const storeDescription = - process.env.NEXT_PUBLIC_STORE_DESCRIPTION || - "A modern e-commerce storefront powered by Spree Commerce and Next.js."; interface StoreContextValue { storeName: string; - storeDescription: string; country: string; locale: string; currency: string; @@ -94,7 +90,6 @@ export function StoreProvider({ const value = useMemo( () => ({ storeName, - storeDescription, country, locale, currency, From 2ec6e2048d9dd44be40d92aa77261b2943c62c11 Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 21:41:33 +0200 Subject: [PATCH 26/28] Prioritize translated description over env var in Footer t("description") from locale files now takes precedence over NEXT_PUBLIC_STORE_DESCRIPTION env var, with env as fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/layout/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 12f7be69..98b55ead 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -28,7 +28,7 @@ export async function Footer({
    {storeName}

    - {envStoreDescription || t("description")} + {t("description") || envStoreDescription}

    From 38c240a8ee71bcbba60858fc429a44cd838f775f Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Tue, 7 Apr 2026 21:57:54 +0200 Subject: [PATCH 27/28] Move CartDrawer into [locale] layout and restore i18n translations CartDrawer was in root layout (outside NextIntlClientProvider) so all its strings were hardcoded English. Moved CartDrawer + Toaster into [country]/[locale]/layout.tsx inside the i18n provider, restored all useTranslations calls. CartProvider stays in root layout (no i18n needed). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/[country]/[locale]/layout.tsx | 4 ++++ src/app/layout.tsx | 8 +------ src/components/cart/CartDrawer.tsx | 31 ++++++++++++++++----------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/app/[country]/[locale]/layout.tsx b/src/app/[country]/[locale]/layout.tsx index 7763cafc..53b37c14 100644 --- a/src/app/[country]/[locale]/layout.tsx +++ b/src/app/[country]/[locale]/layout.tsx @@ -1,7 +1,9 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { NextIntlClientProvider } from "next-intl"; +import { CartDrawer } from "@/components/cart/CartDrawer"; import { JsonLd } from "@/components/seo/JsonLd"; +import { Toaster } from "@/components/ui/sonner"; import { AuthProvider } from "@/contexts/AuthContext"; import { StoreProvider } from "@/contexts/StoreContext"; import { getMarkets } from "@/lib/data/markets"; @@ -82,6 +84,8 @@ export default async function CountryLocaleLayout({ {children} + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3dcdd6b8..b8cfdd4f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,8 +3,6 @@ import type { Metadata } from "next"; import { Geist } from "next/font/google"; import "./globals.css"; import { Suspense } from "react"; -import { CartDrawer } from "@/components/cart/CartDrawer"; -import { Toaster } from "@/components/ui/sonner"; import { CartProvider } from "@/contexts/CartContext"; const gtmId = process.env.GTM_ID; @@ -38,11 +36,7 @@ export default function RootLayout({ className={`${geist.variable} antialiased min-h-screen flex flex-col`} > - - {children} - - - + {children} diff --git a/src/components/cart/CartDrawer.tsx b/src/components/cart/CartDrawer.tsx index a499f9ad..525399bc 100644 --- a/src/components/cart/CartDrawer.tsx +++ b/src/components/cart/CartDrawer.tsx @@ -3,6 +3,7 @@ import { ShoppingBag, Trash, X } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; @@ -29,6 +30,8 @@ export function CartDrawer() { removeItem, itemCount, } = useCart(); + const t = useTranslations("cart"); + const tc = useTranslations("common"); const pathname = usePathname(); const basePath = extractBasePath(pathname); const viewCartFiredRef = useRef(false); @@ -77,16 +80,18 @@ export function CartDrawer() { - Cart + {t("cart")} {itemCount > 0 && ( - ({itemCount} items) + + {t("itemCount", { count: itemCount })} + )} @@ -110,13 +115,13 @@ export function CartDrawer() { className="w-16 h-16 text-gray-300 mb-4" strokeWidth={1} /> -

    Your cart is empty

    +

    {t("emptyCart")}

    - Continue Shopping + {tc("continueShopping")}
    ) : ( @@ -159,7 +164,7 @@ export function CartDrawer() { } }} disabled={updating} - aria-label="Remove item" + aria-label={t("removeItemLabel", { name: item.name })} > @@ -218,18 +223,20 @@ export function CartDrawer() { {/* Summary */}
    - Subtotal + {tc("subtotal")} {cart?.display_item_total}
    {cart?.discount_total && parseFloat(cart.discount_total) < 0 && (
    - Discount + {tc("discount")} {cart.display_discount_total}
    )}
    - Shipping - Calculated on checkout + {tc("shipping")} + + {t("shippingCalculatedAtCheckout")} +
    @@ -240,12 +247,12 @@ export function CartDrawer() { href={`${basePath}/checkout/${cart?.id}`} onClick={closeCart} > - Checkout + {t("checkout")}
    From 3e3e1a3df260854c1235340e46e707c2b31b752b Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Wed, 8 Apr 2026 12:26:12 +0200 Subject: [PATCH 28/28] Add @swc/helpers as direct dep to fix npm ci next-intl's @swc/core requires @swc/helpers>=0.5.17 but only 0.5.15 was resolved transitively. Adding it directly ensures the correct version is hoisted. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 27 ++++++++++++++++++++++++--- package.json | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fdfee76..0fa3b800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@spree/sdk": "^0.20.2", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", + "@swc/helpers": "^0.5.21", "@vercel/analytics": "^2.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -5405,6 +5406,16 @@ "node": ">= 10" } }, + "node_modules/@react-email/preview-server/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@react-email/preview-server/node_modules/next": { "version": "16.1.7", "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", @@ -6738,10 +6749,11 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -11501,6 +11513,15 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index d6f95f82..9ca041c1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@spree/sdk": "^0.20.2", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", + "@swc/helpers": "^0.5.21", "@vercel/analytics": "^2.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -57,8 +58,8 @@ "lefthook": "^2.1.2", "react-email": "^5.2.10", "tailwindcss": "^4", - "typescript": "^5", "tsx": "^4.19.0", + "typescript": "^5", "vitest": "^4.0.18" } }