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",
+};