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:
@@ -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|.*\\..*).*)",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user