refactor: centralize locale constants to prevent breaking changes

Created src/lib/i18n/locales.ts as single source of truth for:
- SUPPORTED_LOCALES array
- LOCALE_COOKIE name
- DEFAULT_LOCALE
- LOCALE_CONFIG (labels, flags, Saleor locale mapping)
- Helper functions (isValidLocale, getSaleorLocale, getLocaleFromPath)

Updated all files to use centralized constants:
- middleware.ts
- Header.tsx
- ProductCard.tsx
- sitemap.ts
- root layout and locale layout
- routing.ts

Benefits:
- Adding new locale only requires updating ONE file (locales.ts)
- No more hardcoded locale lists scattered across codebase
- Cookie name defined in one place
- Type-safe locale validation
This commit is contained in:
Unchained
2026-03-24 11:27:55 +02:00
parent a4e7a07adb
commit a5cd048a6e
8 changed files with 92 additions and 81 deletions

View File

@@ -9,11 +9,8 @@ import { useTranslations } from "next-intl";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { User, ShoppingBag, Menu, X, Globe } from "lucide-react";
import CartDrawer from "@/components/cart/CartDrawer";
const LOCALES = [
{ code: "sr", label: "Srpski", flag: "🇷🇸" },
{ code: "en", label: "English", flag: "🇬🇧" },
] as const;
import { SUPPORTED_LOCALES, LOCALE_COOKIE, LOCALE_CONFIG, isValidLocale } from "@/lib/i18n/locales";
import type { Locale } from "@/lib/i18n/locales";
interface HeaderProps {
locale?: string;
@@ -29,9 +26,7 @@ export default function Header({ locale = "sr" }: HeaderProps) {
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
const itemCount = getLineCount();
const localePath = `/${locale}`;
const currentLocale = LOCALES.find((l) => l.code === locale) || LOCALES[0];
const currentLocale = isValidLocale(locale) ? LOCALE_CONFIG[locale] : LOCALE_CONFIG.sr;
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -48,7 +43,11 @@ export default function Header({ locale = "sr" }: HeaderProps) {
setLangDropdownOpen(false);
return;
}
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`;
if (!isValidLocale(newLocale)) {
setLangDropdownOpen(false);
return;
}
document.cookie = `${LOCALE_COOKIE}=${newLocale}; path=/; max-age=31536000`;
const pathWithoutLocale = pathname.replace(/^\/(sr|en|de|fr)/, "") || "/";
const newPath = `/${newLocale}${pathWithoutLocale}`;
window.location.replace(newPath);
@@ -115,7 +114,7 @@ export default function Header({ locale = "sr" }: HeaderProps) {
))}
</nav>
<Link href={localePath || "/"} className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Link href={`/${locale}`} className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
@@ -144,16 +143,16 @@ export default function Header({ locale = "sr" }: HeaderProps) {
exit={{ opacity: 0, y: -10 }}
className="absolute right-0 top-full mt-1 bg-white border border-[#e5e5e5] shadow-lg rounded-md overflow-hidden z-50"
>
{LOCALES.map((loc) => (
{SUPPORTED_LOCALES.map((loc) => (
<button
key={loc.code}
onClick={() => switchLocale(loc.code)}
key={loc}
onClick={() => switchLocale(loc)}
className={`flex items-center gap-2 px-4 py-2 text-sm hover:bg-black/5 transition-colors w-full text-left ${
loc.code === locale ? "bg-black/5 font-medium" : ""
loc === locale ? "bg-black/5 font-medium" : ""
}`}
>
<span>{loc.flag}</span>
<span>{loc.label}</span>
<span>{LOCALE_CONFIG[loc].flag}</span>
<span>{LOCALE_CONFIG[loc].label}</span>
</button>
))}
</motion.div>
@@ -195,7 +194,7 @@ export default function Header({ locale = "sr" }: HeaderProps) {
>
<div className="container h-full flex flex-col">
<div className="flex items-center justify-between h-[72px]">
<Link href={localePath || "/"} onClick={() => setMobileMenuOpen(false)}>
<Link href={`/${locale}`} onClick={() => setMobileMenuOpen(false)}>
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
@@ -260,4 +259,4 @@ export default function Header({ locale = "sr" }: HeaderProps) {
<CartDrawer />
</>
);
}
}

View File

@@ -6,6 +6,7 @@ import Link from "next/link";
import { useTranslations } from "next-intl";
import type { Product } from "@/types/saleor";
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
import { isValidLocale, getSaleorLocale } from "@/lib/i18n/locales";
interface ProductCardProps {
product: Product;
@@ -13,13 +14,13 @@ interface ProductCardProps {
locale?: string;
}
export default function ProductCard({ product, index = 0, locale = "SR" }: ProductCardProps) {
export default function ProductCard({ product, index = 0, locale = "sr" }: ProductCardProps) {
const t = useTranslations("ProductCard");
const image = getProductImage(product);
const price = getProductPrice(product);
const localized = getLocalizedProduct(product, locale);
const saleorLocale = isValidLocale(locale) ? getSaleorLocale(locale) : "SR";
const localized = getLocalizedProduct(product, saleorLocale);
const isAvailable = product.variants?.[0]?.quantityAvailable > 0;
const urlLocale = locale === "SR" ? "sr" : "en";
return (
<motion.div
@@ -28,7 +29,7 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Link href={`/${urlLocale}/products/${localized.slug}`} className="group block">
<Link href={`/${locale}/products/${localized.slug}`} className="group block">
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
{image ? (
<img
@@ -75,4 +76,4 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
</Link>
</motion.div>
);
}
}