From 03becb6ce7e44135bbc56640473b2b982451b4ea Mon Sep 17 00:00:00 2001 From: Unchained Date: Tue, 24 Mar 2026 11:52:22 +0200 Subject: [PATCH] refactor: make locale handling truly centralized and robust - Added getPathWithoutLocale() and buildLocalePath() helpers to locales.ts - Updated Header to use centralized helpers instead of hardcoded regex - Updated middleware to use SUPPORTED_LOCALES in matcher config - Updated LocaleProvider to use isValidLocale() instead of hardcoded array To add a new language now, only update: 1. SUPPORTED_LOCALES in locales.ts 2. LOCALE_CONFIG entry with label, flag, saleorLocale 3. Add translation keys to all message files All routing now uses centralized constants - no more hardcoded locale lists. --- middleware.ts | 38 ++++++++++----------- src/components/layout/Header.tsx | 6 ++-- src/components/providers/LocaleProvider.tsx | 4 +-- src/lib/i18n/locales.ts | 15 +++++++- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/middleware.ts b/middleware.ts index bca233b..1d3229f 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,25 +1,29 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE, getLocaleFromPath } from "@/lib/i18n/locales"; +import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE, getPathWithoutLocale, buildLocalePath, isValidLocale } from "@/lib/i18n/locales"; +import type { Locale } from "@/lib/i18n/locales"; const OLD_SERBIAN_PATHS = ["products", "about", "contact", "checkout"]; +function detectLocale(cookieLocale: string | undefined, acceptLanguage: string): Locale { + if (cookieLocale && isValidLocale(cookieLocale)) { + return cookieLocale; + } + if (acceptLanguage.includes("en")) { + return "en"; + } + return DEFAULT_LOCALE; +} + export default function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value; const acceptLanguage = request.headers.get("accept-language") || ""; if (pathname === "/" || pathname === "") { - let locale = DEFAULT_LOCALE; - - if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as typeof SUPPORTED_LOCALES[number])) { - locale = cookieLocale; - } else if (acceptLanguage.includes("en")) { - locale = "en"; - } - + const locale = detectLocale(cookieLocale, acceptLanguage); const url = request.nextUrl.clone(); - url.pathname = `/${locale}`; + url.pathname = buildLocalePath(locale, "/"); return NextResponse.redirect(url, 301); } @@ -28,16 +32,10 @@ export default function middleware(request: NextRequest) { ); if (isOldSerbianPath) { - let locale = DEFAULT_LOCALE; - - if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as typeof SUPPORTED_LOCALES[number])) { - locale = cookieLocale; - } else if (acceptLanguage.includes("en")) { - locale = "en"; - } - + const locale = detectLocale(cookieLocale, acceptLanguage); + const newPath = buildLocalePath(locale, pathname); const url = request.nextUrl.clone(); - url.pathname = `/${locale}${pathname}`; + url.pathname = newPath; return NextResponse.redirect(url, 301); } @@ -47,7 +45,7 @@ export default function middleware(request: NextRequest) { export const config = { matcher: [ "/", - "/(sr|en|de|fr)/:path*", + `/${SUPPORTED_LOCALES.join("|")}/:path*`, "/((?!api|_next|_vercel|.*\\..*).*)", ], }; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 8b14527..c9bbe59 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -9,7 +9,7 @@ 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"; -import { SUPPORTED_LOCALES, LOCALE_COOKIE, LOCALE_CONFIG, isValidLocale } from "@/lib/i18n/locales"; +import { SUPPORTED_LOCALES, LOCALE_COOKIE, LOCALE_CONFIG, isValidLocale, getPathWithoutLocale, buildLocalePath } from "@/lib/i18n/locales"; import type { Locale } from "@/lib/i18n/locales"; interface HeaderProps { @@ -48,8 +48,8 @@ export default function Header({ locale = "sr" }: HeaderProps) { return; } document.cookie = `${LOCALE_COOKIE}=${newLocale}; path=/; max-age=31536000`; - const pathWithoutLocale = pathname.replace(/^\/(sr|en|de|fr)/, "") || "/"; - const newPath = `/${newLocale}${pathWithoutLocale}`; + const pathWithoutLocale = getPathWithoutLocale(pathname); + const newPath = buildLocalePath(newLocale as Locale, pathWithoutLocale); window.location.replace(newPath); setLangDropdownOpen(false); }; diff --git a/src/components/providers/LocaleProvider.tsx b/src/components/providers/LocaleProvider.tsx index c4ccaae..be319e3 100644 --- a/src/components/providers/LocaleProvider.tsx +++ b/src/components/providers/LocaleProvider.tsx @@ -3,6 +3,7 @@ import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; import { notFound } from "next/navigation"; +import { SUPPORTED_LOCALES, isValidLocale } from "@/lib/i18n/locales"; export default async function LocaleProvider({ children, @@ -11,8 +12,7 @@ export default async function LocaleProvider({ children: React.ReactNode; locale: string; }) { - const locales = ["en", "sr"]; - if (!locales.includes(locale)) notFound(); + if (!isValidLocale(locale)) notFound(); const messages = await getMessages(); diff --git a/src/lib/i18n/locales.ts b/src/lib/i18n/locales.ts index 094b26a..864c2c6 100644 --- a/src/lib/i18n/locales.ts +++ b/src/lib/i18n/locales.ts @@ -21,6 +21,19 @@ export function getSaleorLocale(locale: Locale): string { } export function getLocaleFromPath(pathname: string): string { - const match = pathname.match(/^\/(sr|en|de|fr)/); + const pattern = SUPPORTED_LOCALES.join("|"); + const match = pathname.match(new RegExp(`^\\/(${pattern})`)); return match ? match[1] : DEFAULT_LOCALE; } + +export function getPathWithoutLocale(pathname: string): string { + const pattern = SUPPORTED_LOCALES.join("|"); + return pathname.replace(new RegExp(`^\\/(${pattern})`), "") || "/"; +} + +export function buildLocalePath(locale: Locale, path: string): string { + if (locale === DEFAULT_LOCALE) { + return path === "/" ? "/" : path; + } + return `/${locale}${path === "/" ? "" : path}`; +}