diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..6611433 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,76 @@ +import { match as matchLocale } from "@formatjs/intl-localematcher" +import Negotiator from "negotiator" +import type { NextRequest } from "next/server" +import { NextResponse } from "next/server" +import { i18n } from "./lib/i18n/config" + +function getLocale(request: NextRequest): string { + // Check for saved locale in cookie first + const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value + if (cookieLocale && i18n.locales.includes(cookieLocale as any)) { + return cookieLocale + } + + // Negotiator expects plain object so we need to transform headers + const negotiatorHeaders: Record = {} + request.headers.forEach((value, key) => { + negotiatorHeaders[key] = value + }) + + // @ts-expect-error locales are readonly + const locales: string[] = i18n.locales + + // Use negotiator and intl-localematcher to get best locale + const languages = new Negotiator({ headers: negotiatorHeaders }).languages( + locales, + ) + + const locale = matchLocale(languages, locales, i18n.defaultLocale) + return locale +} + +export function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname + + // Skip API routes, static files, and Next.js internals + if ( + pathname.startsWith("/api/") || + pathname.startsWith("/_next/") || + pathname.includes("/favicon") || + /\.(.*)$/.test(pathname) + ) { + return + } + + // Check if there is any supported locale in the pathname + const pathnameIsMissingLocale = i18n.locales.every( + (locale) => + !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`, + ) + + // Redirect if there is no locale + if (pathnameIsMissingLocale) { + const locale = getLocale(request) + + // Redirect to localized path + const response = NextResponse.redirect( + new URL( + `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`, + request.url, + ), + ) + + // Set locale cookie for future visits + response.cookies.set("NEXT_LOCALE", locale, { + maxAge: 60 * 60 * 24 * 365, // 1 year + path: "/", + }) + + return response + } +} + +export const config = { + // Matcher ignoring `/_next/` and `/api/` + matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], +}