diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 00000000..fb4453a1 --- /dev/null +++ b/messages/de.json @@ -0,0 +1,438 @@ +{ + "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 {storeName}", + "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", + "removeItemLabel": "{name} entfernen" + }, + "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...", + "enterShippingAddressForMethods": "Geben Sie Ihre Lieferadresse ein, um verfügbare Versandmethoden anzuzeigen.", + "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.", + "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.", + "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", + "noOrder": "Keine Bestellung", + "confirmingPayment": "Ihre Zahlung wird bestätigt..." + }, + "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", + "deleteAddressTitle": "Adresse löschen?", + "delete": "Löschen", + "deleting": "Wird gelöscht..." + }, + "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", + "forgotPassword": "Passwort vergessen?" + }, + "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", + "unexpectedError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.", + "firstName": "Vorname", + "lastName": "Nachname" + }, + "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.", + "unknownShipmentStatus": "Unbekannt", + "orderTitle": "Bestellung #{number}", + "shipmentCanceledRefund": "Sendung storniert — eine Rückerstattung wurde veranlasst.", + "shippingMethodUnavailable": "Nicht verfügbar" + }, + "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.", + "qty": "Menge: {quantity}" + }, + "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", + "email": "E-Mail" + }, + "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}", + "removePaymentMethodTitle": "Zahlungsmethode entfernen?", + "endingIn": "endet auf", + "cardEndingIn": "{label} endet auf {digits}", + "cardExpires": "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." + }, + "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 new file mode 100644 index 00000000..6df82b92 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,438 @@ +{ + "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 {storeName}", + "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", + "removeItemLabel": "Remove {name}" + }, + "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...", + "enterShippingAddressForMethods": "Enter your shipping address to view available shipping methods.", + "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.", + "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.", + "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", + "noOrder": "No order", + "confirmingPayment": "Confirming your payment..." + }, + "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", + "deleteAddressTitle": "Delete address?", + "delete": "Delete", + "deleting": "Deleting..." + }, + "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", + "forgotPassword": "Forgot 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", + "unexpectedError": "An unexpected error occurred. Please try again.", + "firstName": "First name", + "lastName": "Last name" + }, + "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", + "unknownShipmentStatus": "Unknown", + "orderTitle": "Order #{number}", + "shipmentCanceledRefund": "Shipment canceled — a refund has been issued.", + "shippingMethodUnavailable": "Unavailable" + }, + "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.", + "qty": "Qty: {quantity}" + }, + "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", + "email": "Email" + }, + "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}", + "removePaymentMethodTitle": "Remove payment method?", + "endingIn": "ending in", + "cardEndingIn": "{label} ending in {digits}", + "cardExpires": "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." + }, + "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 new file mode 100644 index 00000000..ac005fe6 --- /dev/null +++ b/messages/pl.json @@ -0,0 +1,438 @@ +{ + "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 {storeName}", + "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", + "removeItemLabel": "Usuń {name}" + }, + "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...", + "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}", + "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.", + "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.", + "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", + "noOrder": "Brak zamówienia", + "confirmingPayment": "Potwierdzanie 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", + "deleteAddressTitle": "Usunąć adres?", + "delete": "Usuń", + "deleting": "Usuwanie..." + }, + "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", + "forgotPassword": "Zapomniałeś hasła?" + }, + "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ę", + "unexpectedError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.", + "firstName": "Imię", + "lastName": "Nazwisko" + }, + "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", + "unknownShipmentStatus": "Nieznany", + "orderTitle": "Zamówienie #{number}", + "shipmentCanceledRefund": "Przesyłka anulowana — zwrot został wystawiony.", + "shippingMethodUnavailable": "Niedostępna" + }, + "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.", + "qty": "Ilość: {quantity}" + }, + "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", + "email": "E-mail" + }, + "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}", + "removePaymentMethodTitle": "Usunąć metodę płatności?", + "endingIn": "kończąca się na", + "cardEndingIn": "{label} kończąca się na {digits}", + "cardExpires": "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." + }, + "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/next.config.ts b/next.config.ts index aafb9333..7c4545d8 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: { @@ -44,8 +47,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, @@ -63,4 +68,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 0a1c19c3..09c81116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,12 @@ "@spree/sdk": "^0.10.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", @@ -180,6 +182,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 +866,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -903,6 +907,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1649,6 +1654,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 +2301,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 +2548,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 +2645,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 +2667,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 +2680,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 +3104,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 +3121,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 +3139,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 +3159,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 +5373,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", @@ -5484,77 +5848,255 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@spree/next": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@spree/next/-/next-0.10.5.tgz", - "integrity": "sha512-+D3TST5K2iogTGJKoCGcP2aw5wgMkhorvS+bkeYlToMyPuhSGRYSyK8CFZN8Ctt5Uhc+HqYhNBbZi3cYXGYn8w==", - "license": "MIT", - "peerDependencies": { - "@spree/sdk": ">=0.10.1", - "next": ">=15.0.0", - "react": ">=19.0.0" + "node_modules/@spree/next": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@spree/next/-/next-0.10.5.tgz", + "integrity": "sha512-+D3TST5K2iogTGJKoCGcP2aw5wgMkhorvS+bkeYlToMyPuhSGRYSyK8CFZN8Ctt5Uhc+HqYhNBbZi3cYXGYn8w==", + "license": "MIT", + "peerDependencies": { + "@spree/sdk": ">=0.10.1", + "next": ">=15.0.0", + "react": ">=19.0.0" + } + }, + "node_modules/@spree/sdk": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@spree/sdk/-/sdk-0.10.1.tgz", + "integrity": "sha512-TZd4zSy44htGo6KIR1kZR4G45TfRN1Yi3eNmqfsc1+xzQ7y1OFFoR1VmWhiTvZ+MO7NKGrtZv9nxJJaXw1mzHA==", + "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/@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/@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": ">=10" } }, - "node_modules/@spree/sdk": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@spree/sdk/-/sdk-0.10.1.tgz", - "integrity": "sha512-TZd4zSy44htGo6KIR1kZR4G45TfRN1Yi3eNmqfsc1+xzQ7y1OFFoR1VmWhiTvZ+MO7NKGrtZv9nxJJaXw1mzHA==", - "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": ">=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/@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", - "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" } }, + "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 +6538,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 +6617,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 +6627,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 +6642,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 +6688,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6160,6 +6699,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6332,7 +6872,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 +6881,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 +6910,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 +6929,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 +6938,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 +6946,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 +6969,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 +6982,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 +6994,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 +7008,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 +7017,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 +7068,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 +7090,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 +7114,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 +7131,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 +7148,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 +7170,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6813,6 +7334,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6831,8 +7353,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 +7464,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 +7832,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 +7920,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 +7945,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 +8147,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 +8173,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 +8185,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 +8194,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 +8218,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 +8738,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 +8772,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 +8811,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 +8911,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 +8979,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 +9225,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 +9834,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 +9921,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9481,7 +10016,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 +10025,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 +10220,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 +10276,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", @@ -9753,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", @@ -9781,6 +10399,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 +10860,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 +10964,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 +11206,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 +11216,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 +11229,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 +11427,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 +11558,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 +11987,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 +12168,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 +12251,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 +12269,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 +12301,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 +12515,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12004,6 +12630,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 +12725,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12243,7 +12891,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 +12923,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 +12971,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 +12979,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 +13262,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 38c843bb..a33f7d3c 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", @@ -19,10 +20,12 @@ "@spree/sdk": "^0.10.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", 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 e9b968e4..9ca19cef 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx @@ -4,6 +4,7 @@ import type { Address, AddressParams, Cart, Country } from "@spree/sdk"; import { CircleAlert, Loader2 } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { use, useCallback, useEffect, useRef, useState } from "react"; import { AddressSection } from "@/components/checkout/AddressSection"; import { CouponCode } from "@/components/checkout/CouponCode"; @@ -79,6 +80,8 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const searchParams = useSearchParams(); const basePath = extractBasePath(pathname); const { setSummaryContent } = useCheckout(); + const t = useTranslations("checkout"); + const tc = useTranslations("common"); // Pick up payment errors from the confirm-payment redirect const paymentError = searchParams.get("payment_error"); @@ -103,27 +106,33 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const paymentRef = useRef(null); // Handle coupon code application - const handleApplyCoupon = useCallback(async (code: string) => { - const currentOrder = cartRef.current; - if (!currentOrder) return { success: false, error: "No cart" }; + const handleApplyCoupon = useCallback( + async (code: string) => { + const currentOrder = cartRef.current; + if (!currentOrder) return { success: false, error: t("noOrder") }; - const result = await applyCouponCode(currentOrder.id, code); - if (result.success && result.cart) { - setCart(result.cart); - } - return result; - }, []); + const result = await applyCouponCode(currentOrder.id, code); + if (result.success && result.cart) { + setCart(result.cart); + } + return result; + }, + [t], + ); - const handleRemoveCoupon = useCallback(async (couponCode: string) => { - const currentOrder = cartRef.current; - if (!currentOrder) return { success: false, error: "No cart" }; + const handleRemoveCoupon = useCallback( + async (couponCode: string) => { + const currentOrder = cartRef.current; + if (!currentOrder) return { success: false, error: t("noOrder") }; - const result = await removeCouponCode(currentOrder.id, couponCode); - if (result.success && result.cart) { - setCart(result.cart); - } - return result; - }, []); + const result = await removeCouponCode(currentOrder.id, couponCode); + if (result.success && result.cart) { + setCart(result.cart); + } + return result; + }, + [t], + ); // Track cart key for sidebar updates const cartKey = cart ? `${cart.id}-${cart.updated_at}` : null; @@ -171,7 +180,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { : { data: [] as Country[] }; if (!cartData) { - setError("Order not found or you don't have access to it."); + setError(t("orderNotFound")); setLoading(false); return; } @@ -197,12 +206,12 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { return cartData; } catch { - setError("Failed to load checkout. Please try again."); + setError(t("failedToLoadCheckout")); return null; } finally { setLoading(false); } - }, [cartId, urlCountry, basePath, router, paymentError]); + }, [cartId, urlCountry, basePath, router, paymentError, t]); useEffect(() => { loadOrder(); @@ -251,7 +260,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { }); if (!updateResult.success) { - setError(updateResult.error || "Failed to save address"); + setError(updateResult.error || t("failedToSaveAddress")); return; } @@ -259,12 +268,12 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { setCart(updateResult.cart); } } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); } finally { setSaving(false); } }, - [], + [t], ); // Handle shipping rate selection @@ -286,7 +295,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { rateId, ); if (!result.success) { - setError(result.error || "Failed to select shipping rate"); + setError(result.error || t("failedToSelectRate")); } else if (result.cart) { setCart(result.cart); @@ -297,7 +306,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { trackingRateName = selectedRate?.name; } } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); } finally { setProcessing(false); } @@ -310,7 +319,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { } } }, - [], + [t], ); // Handle billing address update (called by PaymentSection before gateway confirmation) @@ -327,17 +336,17 @@ 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; } }, - [], + [t], ); // Handle payment completion (called by PaymentSection after Stripe confirms) @@ -355,7 +364,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; } @@ -370,7 +379,7 @@ 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 || "Failed to complete order"); + setError(completeResult.error || t("failedToCompleteOrder")); setProcessing(false); return; } @@ -385,11 +394,11 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { router.push(`${basePath}/order-placed/${currentOrder.id}`); } catch { - setError("An error occurred. Please try again."); + setError(t("generalError")); setProcessing(false); } }, - [basePath, router], + [basePath, router, t], ); // Fetch states for a country @@ -408,16 +417,16 @@ 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")); } return result.address; }, - [], + [t], ); // Validate and pay — single "Pay now" action @@ -430,7 +439,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // Refresh cart to get latest requirements const freshOrder = await getCheckoutOrder(cart.id); if (!freshOrder) { - setError("Failed to load cart. Please try again."); + setError(t("failedToLoadCheckout")); return; } setCart(freshOrder); @@ -472,7 +481,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { // All requirements met — submit payment if (!paymentRef.current) { - setError("Payment is not ready. Please wait and try again."); + setError(t("failedToInitPayment")); return; } @@ -501,14 +510,14 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { return (

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

{error}

- Return to Cart + {t("returnToCart")}
); @@ -521,16 +530,14 @@ 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 + {tc("continueShopping")}
); @@ -604,10 +611,10 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { {processing ? ( <> - Processing... + {tc("processing")} ) : ( - "Pay now" + t("payNow") )} 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]/(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"), ); }); }); diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index 81b88338..1760c734 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 (
@@ -58,17 +62,19 @@ function MobileSummaryToggle() { type="button" 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" })} > - {isOpen ? "Hide order summary" : "Show order summary"} + {isOpen ? t("hideOrderSummary") : t("showOrderSummary")} {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 57a92db1..26341553 100644 --- a/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/order-placed/[id]/page.tsx @@ -4,6 +4,7 @@ import type { Cart } from "@spree/sdk"; import { CircleCheckBig } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { use, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { ProductImage } from "@/components/ui/product-image"; @@ -26,6 +27,8 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { const pathname = usePathname(); const basePath = extractBasePath(pathname); const { setSummaryContent } = useCheckout(); + const t = useTranslations("orderPlaced"); + const tc = useTranslations("common"); const [order, setOrder] = useState(null); const [loading, setLoading] = useState(true); @@ -62,13 +65,13 @@ 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); } } @@ -79,7 +82,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { return () => { cancelled = true; }; - }, [cartId]); + }, [cartId, t]); if (loading) { return ( @@ -96,10 +99,10 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { return (

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

); @@ -114,19 +117,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) => ( @@ -147,7 +153,9 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) { {item.options_text && (

    {item.options_text}

    )} -

    Qty: {item.quantity}

    +

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

{item.display_total} @@ -159,27 +167,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} @@ -191,7 +199,7 @@ export default function OrderPlacedPage({ params }: OrderPlacedPageProps) {

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

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

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

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

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

@@ -259,7 +267,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 44463da9..7321c035 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,9 @@ function AddressCard({ onEdit: () => void; onDelete: () => void; }) { + const t = useTranslations("account"); + const tc = useTranslations("common"); + const ta = useTranslations("address"); const [deleting, setDeleting] = useState(false); const handleDelete = async () => { @@ -57,26 +61,25 @@ function AddressCard({
- Delete address? + {ta("deleteAddressTitle")} - This will permanently delete this address. This action cannot - be undone. + {t("deleteConfirm")} - Cancel + {tc("cancel")} - Delete + {ta("delete")} @@ -88,6 +91,7 @@ function AddressCard({ } export default function AddressesPage() { + const t = useTranslations("account"); const [addresses, setAddresses] = useState([]); const [countries, setCountries] = useState([]); const [loading, setLoading] = useState(true); @@ -161,7 +165,9 @@ export default function AddressesPage() { if (loading) { return (
-

Addresses

+

+ {t("addresses")} +

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

Addresses

+

{t("addresses")}

@@ -191,12 +197,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 3db500c6..5666dc83 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 () => { @@ -48,10 +51,16 @@ function CreditCardItem({ />

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

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

{card.name && (

{card.name}

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

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

{[1, 2].map((i) => ( @@ -144,17 +154,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 +181,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/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/gift-cards/page.tsx b/src/app/[country]/[locale]/(storefront)/account/gift-cards/page.tsx index 2de995a4..bfe05c7c 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 { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { getGiftCards } from "@/lib/data/gift-cards"; @@ -22,27 +23,10 @@ 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"; +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", @@ -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 () => { @@ -75,17 +60,17 @@ function CopyButton({ code }: { code: string }) { variant="ghost" size="sm" onClick={handleCopy} - title="Copy code to clipboard" + title={t("copyCodeToClipboard")} > {copied ? ( <> - Copied! + {t("copied")} ) : ( <> - Copy + {t("copy")} )} @@ -93,11 +78,31 @@ 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) : 0; + function getStateLabel(state: string, expired?: boolean): string { + if (expired) return t("expired"); + 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 (
@@ -110,20 +115,20 @@ function GiftCardItem({ card }: { card: GiftCard }) { - {getStateLabel(card.state)} + {getStateLabel(card.state, card.expired)}

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

{card.display_amount_remaining}

-

remaining

+

{t("remaining")}

@@ -131,9 +136,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, locale) })} + {card.redeemed_at && ( - Fully redeemed on {formatDate(card.redeemed_at)} + + {t("fullyRedeemedOnDate", { + date: formatDate(card.redeemed_at, locale), + })} + )}
@@ -160,6 +175,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 +191,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 +234,7 @@ export default function GiftCardsPage() { {activeCards.length > 0 && (

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

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

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

{inactiveCards.map((card) => ( @@ -245,8 +263,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/layout.tsx b/src/app/[country]/[locale]/(storefront)/account/layout.tsx index 1733437e..9abd8bf3 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 getNavItems(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 = getNavItems(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 4d586108..82df64ad 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 { 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"; @@ -18,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", @@ -31,22 +36,8 @@ 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; if (payment.source_type === "credit_card" && source) { @@ -60,10 +51,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,10 +71,12 @@ function PaymentSourceInfo({ payment }: { payment: Payment }) { const credit = source as StoreCredit; return (
-

Store Credit

+

{t("storeCredit")}

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

); @@ -113,6 +112,7 @@ function LineItemCard({ item: Order["items"][number]; basePath: string; }) { + const t = useTranslations("orders"); return (
{/* Image */} @@ -141,12 +141,14 @@ function LineItemCard({ {item.options_text && (

{item.options_text}

)} -

Qty: {item.quantity}

+

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

- Order again + {t("orderAgain")}
@@ -169,6 +171,7 @@ function ShipmentBlock({ basePath: string; lineItems: Order["items"]; }) { + const t = useTranslations("orders"); return (
{/* Shipment header */} @@ -177,7 +180,7 @@ function ShipmentBlock({ {shipAddress && (

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

@@ -185,20 +188,23 @@ function ShipmentBlock({

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

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

{shipment.stock_location && (

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

)} - {shipment.state} + {t( + SHIPMENT_STATE_KEY[shipment.state] || "unknownShipmentStatus", + )}
@@ -209,12 +215,12 @@ function ShipmentBlock({ target="_blank" rel="noopener noreferrer" > - Track Items + {t("trackItems")} ) : ( )}
@@ -224,16 +230,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")}
)}
@@ -260,6 +264,9 @@ interface OrderDetailPageProps { export default function OrderDetailPage({ params }: OrderDetailPageProps) { const { id } = use(params); + const t = useTranslations("orders"); + const tc = useTranslations("common"); + const locale = useLocale(); const pathname = usePathname(); const basePath = extractBasePath(pathname); const [order, setOrder] = useState(null); @@ -291,16 +298,14 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { return (

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

-

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

+

{t("orderNotFoundDescription")}

- Back to orders + {t("backToOrders")}
); @@ -316,15 +321,15 @@ 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 */}

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

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

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

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

{order.special_instructions}

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

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

@@ -374,7 +379,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") @@ -395,16 +400,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} @@ -412,12 +417,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..60e2dd25 100644 --- a/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/orders/page.tsx @@ -4,51 +4,31 @@ 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 { + getPaymentStatusColor, + getShipmentStatusColor, + PAYMENT_STATE_KEY, + 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: "short", day: "numeric", }); } -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"); + const locale = useLocale(); const pathname = usePathname(); const basePath = extractBasePath(pathname); const [orders, setOrders] = useState([]); @@ -56,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(); }, []); @@ -67,7 +52,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 +94,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 +123,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 +158,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 d051686d..539605f6 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"; @@ -32,6 +33,7 @@ export default function AccountPage() { const pathname = usePathname(); const searchParams = useSearchParams(); const basePath = extractBasePath(pathname); + const t = useTranslations("account"); const { login, isAuthenticated, loading: authLoading } = useAuth(); // Get redirect URL from query params (e.g., from checkout) @@ -55,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); }; @@ -79,10 +81,8 @@ export default function AccountPage() {
- My Account - - Sign in to access your account and order history. - + {t("myAccount")} + {t("signInDescription")} @@ -95,7 +95,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 ? ( @@ -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")}
@@ -158,7 +158,7 @@ export default function AccountPage() { size="lg" className="w-full" > - {loading ? "Signing in..." : "Sign In"} + {loading ? t("signingIn") : t("signIn")}
@@ -166,12 +166,12 @@ export default function AccountPage() {

- Don't have an account?{" "} + {t("dontHaveAccount")}{" "} - Sign up + {t("signUp")}

@@ -184,7 +184,7 @@ export default function AccountPage() { return (

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

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

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

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

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

Addresses

+

+ {t("addresses")} +

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

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

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

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

@@ -247,9 +249,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..5ffad034 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,17 +116,21 @@ function ProfileForm({

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

-
Account ID
+
+ {t("accountId")} +
{user.id}
-
Email
+
+ {t("email")} +
{user.email}
@@ -136,12 +142,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 5e618df6..318b19bb 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"; @@ -23,6 +24,8 @@ export default function RegisterPage() { const router = useRouter(); const pathname = usePathname(); const basePath = extractBasePath(pathname); + const t = useTranslations("register"); + const ta = useTranslations("account"); const { register, isAuthenticated, loading: authLoading } = useAuth(); const [firstName, setFirstName] = useState(""); @@ -52,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; } @@ -74,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); } @@ -87,8 +90,8 @@ export default function RegisterPage() {
- Create Account - Sign up to start shopping with us. + {t("createAccount")} + {t("signUpDescription")} @@ -102,7 +105,7 @@ export default function RegisterPage() {
- First name + {t("firstName")} - Last name + {t("lastName")} - Email + {ta("email")} - Password + {ta("password")}
setShowPassword(!showPassword)} aria-label={ - showPassword ? "Hide password" : "Show password" + showPassword ? ta("hidePassword") : ta("showPassword") } > {showPassword ? ( @@ -173,7 +176,7 @@ export default function RegisterPage() { - Confirm Password + {t("confirmPassword")}
{showPasswordConfirmation ? ( @@ -217,7 +220,7 @@ export default function RegisterPage() { size="lg" className="w-full" > - {submitting ? "Creating account..." : "Create Account"} + {submitting ? t("creatingAccount") : t("createAccount")}
@@ -225,12 +228,12 @@ export default function RegisterPage() {

- Already have an account?{" "} + {t("alreadyHaveAccount")}{" "} - Sign in + {t("signIn")}

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/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 9084468d..ef51fb79 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"; @@ -28,6 +29,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 { @@ -70,7 +72,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..af39ec69 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..a1140604 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -1,9 +1,10 @@ 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"; +import { getStoreName } from "@/lib/seo"; interface HomePageProps { params: Promise<{ @@ -22,6 +23,8 @@ 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 storeName = getStoreName(); return (
@@ -30,17 +33,19 @@ export default async function HomePage({ params }: HomePageProps) {

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

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

@@ -51,10 +56,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..7a16763c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,13 @@ 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 { 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 +16,37 @@ 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 locale = await getLocale(); + 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 519489b3..d9827308 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 { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -49,6 +50,8 @@ export function AddressEditModal({ onClose, title, }: AddressEditModalProps) { + const t = useTranslations("address"); + const tc = useTranslations("common"); const [formData, setFormData] = useState( address ? addressToFormData(address) : { ...emptyAddress }, ); @@ -72,14 +75,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 7f903774..ba2a3d12 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 { Input } from "@/components/ui/input"; import { NativeSelect, @@ -25,6 +26,8 @@ export function AddressFormFields({ onChange, idPrefix, }: AddressFormFieldsProps) { + const t = useTranslations("address"); + const tc = useTranslations("common"); const hasStates = states.length > 0; return ( @@ -33,14 +36,14 @@ export function AddressFormFields({
onChange("country_iso", e.target.value)} required > - Country/Region + {t("selectCountry")} {countries.map((country) => ( @@ -55,19 +58,19 @@ export function AddressFormFields({ onChange("firstname", e.target.value)} - placeholder="First name (optional)" + placeholder={t("firstName")} /> onChange("lastname", e.target.value)} - placeholder="Last name" + placeholder={t("lastName")} />
@@ -75,31 +78,31 @@ export function AddressFormFields({ onChange("company", e.target.value)} - placeholder="Company (optional)" + placeholder={t("company")} /> {/* Address */} onChange("address1", e.target.value)} - placeholder="Address" + placeholder={t("address")} /> {/* Apartment */} onChange("address2", e.target.value)} - placeholder="Apartment, suite, etc. (optional)" + placeholder={t("apartment")} /> {/* City / State / ZIP — 3 columns */} @@ -107,32 +110,32 @@ export function AddressFormFields({ onChange("city", e.target.value)} - placeholder="City" + placeholder={t("city")} /> {loadingStates ? ( - Loading... + {tc("loading")} ) : hasStates ? ( onChange("state_abbr", e.target.value)} required > - State + {t("stateProvince")} {states.map((state) => ( @@ -144,20 +147,20 @@ export function AddressFormFields({ onChange("state_name", e.target.value)} - placeholder="State" + placeholder={t("stateProvince")} /> )} onChange("zipcode", e.target.value)} - placeholder="ZIP code" + placeholder={t("zipCode")} />
@@ -165,10 +168,10 @@ export function AddressFormFields({ onChange("phone", e.target.value)} - placeholder="Phone (optional)" + placeholder={t("phone")} />
); 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/AddressSelector.tsx b/src/components/checkout/AddressSelector.tsx index d3b9ad04..83705873 100644 --- a/src/components/checkout/AddressSelector.tsx +++ b/src/components/checkout/AddressSelector.tsx @@ -2,6 +2,7 @@ import type { Address, Country, State } from "@spree/sdk"; import { MapPin } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useCallback, useMemo } from "react"; import { AddressFormFields } from "@/components/checkout/AddressFormFields"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; @@ -32,6 +33,8 @@ export function AddressSelector({ onFieldBlur, idPrefix, }: AddressSelectorProps) { + const t = useTranslations("address"); + const tc = useTranslations("common"); // Derive selected address from current form data — no useEffect needed const selectedAddressId = useMemo((): string => { if (savedAddresses.length === 0) return "new"; @@ -129,7 +132,7 @@ export function AddressSelector({ }} className="text-xs text-gray-500 underline underline-offset-2 hover:text-gray-900 flex-shrink-0" > - Edit + {tc("edit")} )} @@ -146,7 +149,7 @@ export function AddressSelector({ - Use a different address + {t("useDifferentAddress")} diff --git a/src/components/checkout/CouponCode.tsx b/src/components/checkout/CouponCode.tsx index 36fecd90..faca9532 100644 --- a/src/components/checkout/CouponCode.tsx +++ b/src/components/checkout/CouponCode.tsx @@ -2,6 +2,7 @@ import type { Cart } from "@spree/sdk"; import { X } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -13,6 +14,7 @@ interface CouponCodeProps { } export function CouponCode({ cart, onApply, onRemove }: CouponCodeProps) { + const t = useTranslations("coupon"); const [code, setCode] = useState(""); const [applying, setApplying] = useState(false); const [removing, setRemoving] = useState(null); @@ -34,7 +36,7 @@ export function CouponCode({ cart, onApply, onRemove }: CouponCodeProps) { if (result.success) { setCode(""); } else { - setError(result.error || "Invalid coupon code"); + setError(result.error || t("invalidCode")); } setApplying(false); @@ -46,7 +48,7 @@ export function CouponCode({ cart, onApply, onRemove }: CouponCodeProps) { const result = await onRemove(code); if (!result.success) { - setError(result.error || "Failed to remove coupon code"); + setError(result.error || t("failedToRemove")); } setRemoving(null); @@ -76,7 +78,9 @@ export function CouponCode({ cart, onApply, onRemove }: CouponCodeProps) { )} 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} 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..366822aa 100644 --- a/src/components/navigation/Breadcrumbs.tsx +++ b/src/components/navigation/Breadcrumbs.tsx @@ -1,6 +1,9 @@ +"use client"; + 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 +11,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 +29,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..d1df21ff 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, @@ -69,7 +71,7 @@ export function ProductCarousel({ if (products.length === 0) { return (
-

No products found.

+

{t("noProductsFound")}

); } @@ -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 ec196025..0e7b790d 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); @@ -158,7 +160,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" @@ -166,7 +168,7 @@ export function SearchBar({ basePath }: SearchBarProps) { selectedIndex >= 0 ? `search-option-${selectedIndex}` : undefined } aria-autocomplete="list" - aria-label="Search" + aria-label={t("search")} /> @@ -179,7 +181,7 @@ export function SearchBar({ basePath }: SearchBarProps) {
{loading ? (
- Searching... + {t("searching")}
) : suggestions.length > 0 ? (
    @@ -236,14 +238,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..252aec2e --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,33 @@ +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)) { + // 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"; + } + } + + 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/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"; + } +} 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 {} +}