diff --git a/docs/technical-debt.md b/docs/technical-debt.md index d643dfad..bd94a804 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -14,6 +14,16 @@ Dług dotyczący całego systemu (backend, deploy, dane) żyje w `sklepik/docs/r ## Dług techniczny +### 2026-07-07 — CountrySwitcher zastąpiony LanguageSwitcher — Market switcher jeszcze nie istnieje + +**Status:** w toku (krok 0+1 planu zamknięte, kroki 2-4 czekają na realny drugi rynek) + +**Skrót:** `CountrySwitcher.tsx` mieszał język i walutę w jednym dropdownie i linkował wg usuniętego schematu URL `/{country}/{locale}/...` → 404 na wyborze kraju + wizualny duplikat "PL PL | PLN". Usunięty (razem z `useCountrySwitch.ts`) i zastąpiony `LanguageSwitcher.tsx` — czysto językowym przełącznikiem (pl/en dziś, de/es/fr gotowe strukturalnie w `messages/*.json`, ale niewystawione dopóki ktoś nie zweryfikuje ich jakości), niezależnym od waluty/rynku. Ten sam fix zastosowany w `MobileMenu.tsx` (panel "country" → "language"). + +**Co trzeba zrobić:** zbudować `MarketSwitcher` (waluta + wysyłka, oparty o cookie, nie URL) — dopiero gdy w adminie powstanie realny drugi `Market` (np. Eurozone/EUR). `updateCartMarket` (`src/lib/data/checkout.ts`) zostawiony nieużywany celowo — gotowa, przetestowana funkcja do reużycia w tym kroku. + +**Warunek zamknięcia:** pełny plan w `sklepik/docs/plans/market-language-switcher.md` zrealizowany (kroki 2-4). + ### 2026-07-07 — Zdjęcie główne na stronie produktu czasem się nie wyświetla (mitygacja, nie fix) **Status:** otwarte (frontowa mitygacja wdrożona, przyczyna po stronie backendu/infra pozostaje) diff --git a/messages/de.json b/messages/de.json index 373c3b77..77601bc8 100644 --- a/messages/de.json +++ b/messages/de.json @@ -18,6 +18,7 @@ "continueShopping": "Weiter einkaufen", "orderSummary": "Bestellübersicht", "selectCountry": "Land auswählen", + "selectLanguage": "Sprache auswählen", "myAccount": "Mein Konto", "giftCard": "Geschenkkarte", "storeCredit": "Guthaben", @@ -31,6 +32,7 @@ "openMenu": "Menü öffnen", "menu": "Menü", "selectCountry": "Land auswählen", + "selectLanguage": "Sprache auswählen", "home": "Startseite", "allProducts": "Alle Produkte", "contact": "Kontakt", diff --git a/messages/en.json b/messages/en.json index 5dd69a47..cafbca0e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -18,6 +18,7 @@ "continueShopping": "Continue Shopping", "orderSummary": "Order Summary", "selectCountry": "Select Country", + "selectLanguage": "Select Language", "myAccount": "My Account", "giftCard": "Gift card", "storeCredit": "Store credit", @@ -31,6 +32,7 @@ "openMenu": "Open menu", "menu": "Menu", "selectCountry": "Select Country", + "selectLanguage": "Select Language", "home": "Home", "allProducts": "All Products", "contact": "Contact", diff --git a/messages/es.json b/messages/es.json index 8e7610ce..b83ac324 100644 --- a/messages/es.json +++ b/messages/es.json @@ -18,6 +18,7 @@ "continueShopping": "Seguir comprando", "orderSummary": "Resumen del pedido", "selectCountry": "Seleccionar pais", + "selectLanguage": "Seleccionar idioma", "myAccount": "Mi cuenta", "giftCard": "Tarjeta regalo", "storeCredit": "Credito de la tienda", @@ -31,6 +32,7 @@ "openMenu": "Abrir menu", "menu": "Menu", "selectCountry": "Seleccionar pais", + "selectLanguage": "Seleccionar idioma", "home": "Inicio", "allProducts": "Todos los productos", "contact": "Contacto", diff --git a/messages/fr.json b/messages/fr.json index f36c9878..2e4059d9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -18,6 +18,7 @@ "continueShopping": "Continuer les achats", "orderSummary": "Recapitulatif de la commande", "selectCountry": "Choisir le pays", + "selectLanguage": "Choisir la langue", "myAccount": "Mon compte", "giftCard": "Carte cadeau", "storeCredit": "Credit boutique", @@ -31,6 +32,7 @@ "openMenu": "Ouvrir le menu", "menu": "Menu", "selectCountry": "Choisir le pays", + "selectLanguage": "Choisir la langue", "home": "Accueil", "allProducts": "Tous les produits", "contact": "Contact", diff --git a/messages/pl.json b/messages/pl.json index 86bd1c51..b670a3c6 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -18,6 +18,7 @@ "continueShopping": "Kontynuuj zakupy", "orderSummary": "Podsumowanie zamówienia", "selectCountry": "Wybierz kraj", + "selectLanguage": "Wybierz język", "myAccount": "Moje konto", "giftCard": "Karta podarunkowa", "storeCredit": "Kredyt sklepowy", @@ -31,6 +32,7 @@ "openMenu": "Otwórz menu", "menu": "Menu", "selectCountry": "Wybierz kraj", + "selectLanguage": "Wybierz język", "home": "Strona główna", "allProducts": "Wszystkie produkty", "contact": "Kontakt", diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 6840da9f..c359483a 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -20,10 +20,10 @@ const LazyMobileMenu = dynamic( }, ); -const LazyCountrySwitcher = dynamic( +const LazyLanguageSwitcher = dynamic( () => - import("@/components/layout/CountrySwitcher").then((mod) => ({ - default: mod.CountrySwitcher, + import("@/components/layout/LanguageSwitcher").then((mod) => ({ + default: mod.LanguageSwitcher, })), { loading: () => ( @@ -64,7 +64,7 @@ export async function Header({ } rightStart={
- +
} rightEnd={ diff --git a/src/components/layout/LanguageSwitcher.tsx b/src/components/layout/LanguageSwitcher.tsx new file mode 100644 index 00000000..79c9fcf7 --- /dev/null +++ b/src/components/layout/LanguageSwitcher.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Check, ChevronDown, Globe } from "lucide-react"; +import { usePathname, useRouter } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { getDefaultLocale, getPrefixedLocales } from "@/lib/store"; +import { LANGUAGE_NAMES } from "@/lib/utils/language-names"; +import { buildBasePath, getPathWithoutPrefix } from "@/lib/utils/path"; + +/** + * Purely a UI-language control — independent of currency/market (see + * docs/plans/market-language-switcher.md). Rewrites only the /{locale} + * URL segment via a soft client navigation; never touches cart, currency, + * or cookies. + */ +export function LanguageSwitcher() { + const currentLocale = useLocale(); + const pathname = usePathname(); + const router = useRouter(); + const t = useTranslations("common"); + + const locales = [ + getDefaultLocale() as Locale, + ...getPrefixedLocales().map((l) => l as Locale), + ]; + + // Nothing to switch between — don't render a dropdown with one option. + if (locales.length <= 1) return null; + + const handleSelect = (locale: Locale) => { + if (locale === currentLocale) return; + const pathRest = getPathWithoutPrefix(pathname); + router.push(`${buildBasePath(locale)}${pathRest}`); + }; + + return ( + + + + + + + {t("selectLanguage")} + {locales.map((locale) => { + const isSelected = locale === currentLocale; + return ( + handleSelect(locale)} + > + + {LANGUAGE_NAMES[locale] ?? locale.toUpperCase()} + + {isSelected && } + + ); + })} + + + ); +} diff --git a/src/components/layout/MobileMenu.tsx b/src/components/layout/MobileMenu.tsx index 877cf61a..71b778ea 100644 --- a/src/components/layout/MobileMenu.tsx +++ b/src/components/layout/MobileMenu.tsx @@ -1,9 +1,10 @@ "use client"; import type { Category } from "@spree/sdk"; -import { ArrowLeft, Check, ChevronRight, User, X } from "lucide-react"; +import { ArrowLeft, Check, ChevronRight, Globe, User, X } from "lucide-react"; import Link from "next/link"; -import { useTranslations } from "next-intl"; +import { usePathname, useRouter } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; import { useRef, useState } from "react"; import { flushSync } from "react-dom"; import { Button } from "@/components/ui/button"; @@ -14,22 +15,14 @@ import { SheetFooter, SheetTitle, } from "@/components/ui/sheet"; -import { useStore } from "@/contexts/StoreContext"; -import { useCountrySwitch } from "@/hooks/useCountrySwitch"; - -// Convert ISO country code to flag emoji -function countryToFlag(countryCode: string): string { - const code = countryCode.toUpperCase(); - if (code.length !== 2) return ""; - const firstChar = code.charCodeAt(0) - 65 + 0x1f1e6; - const secondChar = code.charCodeAt(1) - 65 + 0x1f1e6; - return String.fromCodePoint(firstChar, secondChar); -} +import { getDefaultLocale, getPrefixedLocales } from "@/lib/store"; +import { LANGUAGE_NAMES } from "@/lib/utils/language-names"; +import { buildBasePath, getPathWithoutPrefix } from "@/lib/utils/path"; type PanelType = | { kind: "main" } | { kind: "category"; category: Category } - | { kind: "country" }; + | { kind: "language" }; interface MobileMenuProps { rootCategories: Category[]; @@ -46,11 +39,20 @@ export function MobileMenu({ rootCategories, basePath }: MobileMenuProps) { const rafRef = useRef(null); const timeoutRef = useRef(null); - const { country, currency, countries } = useStore(); - const { isCountryNavigating, handleCountrySelect } = useCountrySwitch({ - currentCountry: country, - onBeforeNavigate: () => setOpen(false), - }); + const currentLocale = useLocale(); + const pathname = usePathname(); + const router = useRouter(); + const locales = [ + getDefaultLocale() as Locale, + ...getPrefixedLocales().map((l) => l as Locale), + ]; + + const handleLanguageSelect = (locale: Locale) => { + setOpen(false); + if (locale === currentLocale) return; + const pathRest = getPathWithoutPrefix(pathname); + router.push(`${buildBasePath(locale)}${pathRest}`); + }; const currentPanel = panelStack[panelStack.length - 1]; @@ -191,8 +193,8 @@ export function MobileMenu({ rootCategories, basePath }: MobileMenuProps) { {currentPanel.kind === "category" ? currentPanel.category.name - : currentPanel.kind === "country" - ? t("selectCountry") + : currentPanel.kind === "language" + ? t("selectLanguage") : ""} @@ -262,21 +264,22 @@ export function MobileMenu({ rootCategories, basePath }: MobileMenuProps) { - {/* Footer: Country switcher (mobile + tablet) + Account (mobile only) */} + {/* Footer: Language switcher (mobile + tablet) + Account (mobile only) */} - + {locales.length > 1 && ( + + )} - {t("selectCountry")} + {t("selectLanguage")}
- {countries.map((c) => { - const isSelected = - c.iso.toLowerCase() === country.toLowerCase(); + {locales.map((locale) => { + const isSelected = locale === currentLocale; return ( ); diff --git a/src/lib/utils/language-names.ts b/src/lib/utils/language-names.ts new file mode 100644 index 00000000..0da38b48 --- /dev/null +++ b/src/lib/utils/language-names.ts @@ -0,0 +1,10 @@ +// Each language's own name in its own language (endonym) — a language +// switcher should stay legible regardless of which UI language is +// currently active, so labels are never run through next-intl. +export const LANGUAGE_NAMES: Record = { + pl: "Polski", + en: "English", + de: "Deutsch", + es: "Español", + fr: "Français", +};