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.
This commit is contained in:
Unchained
2026-03-24 11:52:22 +02:00
parent 0a7c555549
commit 03becb6ce7
4 changed files with 37 additions and 26 deletions

View File

@@ -1,25 +1,29 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import type { NextRequest } 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"]; 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) { export default function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname; const pathname = request.nextUrl.pathname;
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value; const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
const acceptLanguage = request.headers.get("accept-language") || ""; const acceptLanguage = request.headers.get("accept-language") || "";
if (pathname === "/" || pathname === "") { if (pathname === "/" || pathname === "") {
let locale = DEFAULT_LOCALE; const locale = detectLocale(cookieLocale, acceptLanguage);
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as typeof SUPPORTED_LOCALES[number])) {
locale = cookieLocale;
} else if (acceptLanguage.includes("en")) {
locale = "en";
}
const url = request.nextUrl.clone(); const url = request.nextUrl.clone();
url.pathname = `/${locale}`; url.pathname = buildLocalePath(locale, "/");
return NextResponse.redirect(url, 301); return NextResponse.redirect(url, 301);
} }
@@ -28,16 +32,10 @@ export default function middleware(request: NextRequest) {
); );
if (isOldSerbianPath) { if (isOldSerbianPath) {
let locale = DEFAULT_LOCALE; const locale = detectLocale(cookieLocale, acceptLanguage);
const newPath = buildLocalePath(locale, pathname);
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as typeof SUPPORTED_LOCALES[number])) {
locale = cookieLocale;
} else if (acceptLanguage.includes("en")) {
locale = "en";
}
const url = request.nextUrl.clone(); const url = request.nextUrl.clone();
url.pathname = `/${locale}${pathname}`; url.pathname = newPath;
return NextResponse.redirect(url, 301); return NextResponse.redirect(url, 301);
} }
@@ -47,7 +45,7 @@ export default function middleware(request: NextRequest) {
export const config = { export const config = {
matcher: [ matcher: [
"/", "/",
"/(sr|en|de|fr)/:path*", `/${SUPPORTED_LOCALES.join("|")}/:path*`,
"/((?!api|_next|_vercel|.*\\..*).*)", "/((?!api|_next|_vercel|.*\\..*).*)",
], ],
}; };

View File

@@ -9,7 +9,7 @@ import { useTranslations } from "next-intl";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { User, ShoppingBag, Menu, X, Globe } from "lucide-react"; import { User, ShoppingBag, Menu, X, Globe } from "lucide-react";
import CartDrawer from "@/components/cart/CartDrawer"; 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"; import type { Locale } from "@/lib/i18n/locales";
interface HeaderProps { interface HeaderProps {
@@ -48,8 +48,8 @@ export default function Header({ locale = "sr" }: HeaderProps) {
return; return;
} }
document.cookie = `${LOCALE_COOKIE}=${newLocale}; path=/; max-age=31536000`; document.cookie = `${LOCALE_COOKIE}=${newLocale}; path=/; max-age=31536000`;
const pathWithoutLocale = pathname.replace(/^\/(sr|en|de|fr)/, "") || "/"; const pathWithoutLocale = getPathWithoutLocale(pathname);
const newPath = `/${newLocale}${pathWithoutLocale}`; const newPath = buildLocalePath(newLocale as Locale, pathWithoutLocale);
window.location.replace(newPath); window.location.replace(newPath);
setLangDropdownOpen(false); setLangDropdownOpen(false);
}; };

View File

@@ -3,6 +3,7 @@
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server"; import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { SUPPORTED_LOCALES, isValidLocale } from "@/lib/i18n/locales";
export default async function LocaleProvider({ export default async function LocaleProvider({
children, children,
@@ -11,8 +12,7 @@ export default async function LocaleProvider({
children: React.ReactNode; children: React.ReactNode;
locale: string; locale: string;
}) { }) {
const locales = ["en", "sr"]; if (!isValidLocale(locale)) notFound();
if (!locales.includes(locale)) notFound();
const messages = await getMessages(); const messages = await getMessages();

View File

@@ -21,6 +21,19 @@ export function getSaleorLocale(locale: Locale): string {
} }
export function getLocaleFromPath(pathname: string): 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; 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}`;
}