[▲ Vercel Community](/) · [Categories](/categories) · [Latest](/latest) · [Top](/top) · [Live](/live) [Help](/c/help/9) # Next-intl (i18n) language reset to previous locale in Vercel 441 views · 0 likes · 3 posts Jacob (@coffsjacob) · 2025-02-24 # Middleware Issue Review (VERCEL) ## Repo https://github.com/miniu-jacob/next-ccunion.git # Overview * After deploying to Vercel, an issue occurs where the language setting resets when I change the language and navigates to a different link. # Background ### Environment: next.js 15 with Turbo, next-intl for multilingual support, Vercel deployment ``` "next": "15.1.7", "react": "^19.0.0", "next-intl": "^3.26.3", ``` ### Problem Situation: When a user changes the language (e.g., from ko to en) and navigates to another link, the language reverts to the default (e.g., ko or en). * In a similar e-commerce project, no issues occurred when using the <Link> component from next/link in the header. * Works correctly locally, but the issue only occurs in the Vercel deployment environment. ### Related Code * middleware: Integration of next-intl and next-auth, handling language and authentication logic. * i18n/routing.ts: Routing configuration for next-intl (localePrefix: "as-needed"). # Cause Analysis ### Client-Server State Inconsistency: Developer Tools → Inspect → Network Logs * /ko/about → 307 Temporary Redirect → /about * /ko/about → /about, next-Url: /vn/about → Confirmed that the client maintains the previous language (vn). * Location: Client router (next-Url - observable in the network request headers in developer tools). * /about → next-Url: /vn/about persists → Next request goes to /vn. ### Suspected Cause * Inconsistency between the client router state (next-Url) and the server middleware. ### Actions for Problem Resolution # 1. What I have tried ``` // i18n/routing.ts localeCookie: { name: "NEXT_LOCALE" }, // <--- did not solve localeDetection: false, // <--- did not solve localePrefix: { mode: "as-needed" }, // <--- did not solve localePrefix: { mode: "as-needed", { prefixes: { "ko": "/", "en": "/en", "vn": "/vn", } }, // <--- did not solve localePrefix: { mode: "always", { prefixes: { "ko": "/ko", "en": "/en", "vn": "/vn", } }, // <--- did not solve localePrefix: "always", // <--- did not solve pathnames: { ... }, // <--- did not solve ``` # i18n/routing.ts file code ```ts // i18n/routing.ts import { routesData } from "@/lib/db/data/routes-data"; import { createNavigation } from "next-intl/navigation"; import { defineRouting } from "next-intl/routing"; // routesData 에서 모든 경로 추출 const allPaths = [ ...routesData.public.static, ...routesData.public.dynamic, ...routesData.protected.static, ...routesData.protected.dynamic, ]; const pathnames = Object.fromEntries(allPaths.map((path) => [path, path])); /* ROUTING CONFIGURATION * ============================= * [Step D] Routing 설정을 정의한다. * - locales: 언어 목록 * - defaultLocale: 기본 언어 * - localeCookie: 쿠키 설정 (선택) * - localePrefix: 언어별 prefix 설정 - 모드 설정 시 pathnames와 관계 없이 prefix 설정 * - pathnames: URL 경로가 다른 경우 필요 * ============================= */ export const routing = defineRouting({ locales: ["ko", "en", "vn"], defaultLocale: "ko", // localeDetection: false, // localeCookie: { name: "NEXT_LOCALE" }, localePrefix: "always", pathnames, }); export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); ``` # Here's the middleware code. # middleware.ts ```ts // middleware.ts import createMiddleware from "next-intl/middleware"; import { routing } from "./i18n/routing"; import NextAuth from "next-auth"; import authConfig from "./lib/auth.config"; import { protectedPaths, publicPaths } from "./lib/db/data/routes-data"; import { NextResponse } from "next/server"; import { isMatch } from "./lib/utils"; /* 다국어 미들웨어 생성(routing 객체 전달, createMiddleware 함수 사용) * ========================================= * [Pre-A] next-intl / next-auth 설정 * ========================================= */ const intlMiddleware = createMiddleware(routing); // nextAuth 인증 미들웨어 래퍼 const { auth } = NextAuth(authConfig); /* ####### MIDDLEWARE FUNCTION ####### * ========================================= * MAIN MIDDLEWARE * ========================================= */ export default auth((req) => { // [Step A] next-intl 미들웨어 (rewrite/locale 설정 등) 먼저 실행 const { pathname } = req.nextUrl; const locales = routing.locales; const defaultLocale = routing.defaultLocale; /* URL PREFIX 추출 * ============================= * [Step A] URL에서 prefix 를 추출한다. * - locales: 언어 목록 * - defaultLocale: 기본 언어 * ============================= */ const pathnameParts = pathname.split("/").filter(Boolean); // 빈 문자열 제거 const prefix = pathnameParts[0] || ""; const pathnameSegments = req.nextUrl.pathname.split("/"); console.log("[middleware - Step B] pathname comes: ", pathname); console.log("[middleware - Step B] prefix extracted: ", pathnameParts); console.log("[middleware - Step B] pathnameSegments: ", pathnameSegments); /* LANGUAGE SETTINGS * ============================= * [Step B] 현재 URL 에서 locale을 감지한다. * - urlLocale : URL에서 추출한 언어(prefix) * - userLocale: 쿠키에서 추출한 언어 * - currentLocale: URL에서 추출한 언어(prefix) -> 쿠키 -> 기본 언어 순서 * ============================= */ const urlLocale = locales.includes(prefix as "ko" | "en" | "vn") ? prefix : null; const userLocale = req.cookies.get("NEXT_LOCALE")?.value; const currentLocale = userLocale || defaultLocale; console.log( "[middleware - Step C]: ", "[urlLocale]: ", urlLocale, "[userLocale]: ", userLocale, "[currentLocale]: ", currentLocale, ); /* PATH CHECK * ============================= * [Step C] 공개/비공개 경로 확인 * - pathWithoutPrefix: prefix를 제거한 경로 * - isPublicRoute: 요청된 경로가 공개 경로인지 확인 * ============================= */ const pathWithoutPrefix = `/${pathnameParts.slice(1).join("/")}` || "/"; const isPublicRoute = isMatch(pathWithoutPrefix, publicPaths); const isProtectedRoute = isMatch(pathWithoutPrefix, protectedPaths); console.log("[middleware - Step D] pathWithoutPrefix: ", pathWithoutPrefix); console.log("[middleware - Step D] isPublicRoute: ", isPublicRoute); console.log("[middleware - Step D] isProtectedRoute: ", isProtectedRoute); // 모든 경로에 대해 intlMiddleware 실행(잘못된 경로는 외부처리) let response = NextResponse.next(); // 기본 응답 생성 /* COOKIE SETTINGS * ============================= * [Step D] NEXT_LOCALE 쿠키 설정 * - 조건에 따라 쿠키를 설정한다. * - a). NEXT_LOCALE 쿠키를 가져오지 못한 경우 * - b). NEXT_LOCALE 쿠키가 있지만, 현재 언어(currentLocale)와 다른 경우 * ============================= */ if (!req.cookies.get("NEXT_LOCALE") || req.cookies.get("NEXT_LOCALE")?.value !== currentLocale) { response.cookies.set("NEXT_LOCALE", currentLocale, { path: "/", maxAge: 60 * 60 * 24 * 30, // 30 days secure: process.env.NODE_ENV === "production", // VERCEL HTTPS support }); console.log("[middleware - Step D] NEXT_LOCALE cookie set early: ", currentLocale); } // 공개 경로 처리 if (isPublicRoute) { return intlMiddleware(req); } // 비공개 경로 처리 if (isProtectedRoute) { if (!req.auth) { const loginUrl = new URL(`/login?callbackUrl=${encodeURIComponent(pathname)}`, req.nextUrl.origin); return NextResponse.redirect(loginUrl); } } // console.log("[middleware - Step D] NEXT_LOCALE cookie set: ", currentLocale); // console.log("[middleware - Final] Response locale: ", currentLocale); response = intlMiddleware(req); return response; }); /* ========================================= * next.js 미들웨어 설정 * ========================================= */ export const config = { // matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"], // matcher: ["/", "/(ko|en|vn)/:path*"], // matcher: ["/((?!api|_next|.*\\..*).*)"], matcher: ["/((?!api|image|_next|favicon.ico|_next/image|icons|_next/static|_vercel\\..*).*)"], }; ``` # Language-switcher.tsx file code ```ts // components/shared/header/language-switcher.tsx "use client"; import * as React from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { i18n } from "@/i18n-config"; import { Link, usePathname } from "@/i18n/routing"; import { useLocale } from "next-intl"; import Image from "next/image"; /** * 브라우저에서 NEXT_LOCALE 쿠키를 세팅하는 함수 * - maxAge: 1년 (필요하면 조정) * - Secure: HTTPS 환경에서만 전송 (Vercel이라면 보통 자동 HTTPS) * - SameSite=Lax */ export function cleanPathname(pathname: string, locale: string) { const localePrefix = `/${locale}`; return pathname.startsWith(localePrefix) ? pathname.replace(localePrefix, "") || "/" : pathname; } export default function LanguageSwitcher() { const { locales } = i18n; const locale = useLocale(); const pathname = usePathname(); // console.log("[DEBUG] LanguageSwitcher - locale: ", locale); function setLocaleCookie(locale: string) { const maxAge = 60 * 60 * 24 * 365; // 1년 document.cookie = `NEXT_LOCALE=${locale}; Path=/; Max-Age=${maxAge}; Secure; SameSite=Lax`; } return ( <DropdownMenu> <DropdownMenuTrigger className="header-button h-16"> <div className="flex items-center gap-1 text-base"> <Image src={locales.find((l) => l.slug === locale)?.icon || locales[0].icon} alt={"language"} width={28} height={28} /> {locale.toUpperCase().slice(0, 2)} </div> </DropdownMenuTrigger> <DropdownMenuContent className="w-40" align="center"> <DropdownMenuLabel>Language</DropdownMenuLabel> <DropdownMenuRadioGroup value={locale}> {locales.map((c) => ( // 현재 언어는 선택 불가능하게 <DropdownMenuRadioItem key={c.name} value={c.slug} disabled={c.slug === locale}> {/* <div className="w-full flex items-center gap-2 text-sm"> <Image src={c.icon} alt={c.name} width={28} height={28} /> {c.name} </div> */} <Link href={pathname} locale={c.slug} className="w-full flex items-center gap-2 text-sm" onClick={() => { setLocaleCookie(c.slug); }}> <Image src={c.icon} alt={c.name} width={28} height={28} /> {c.name} </Link> </DropdownMenuRadioItem> ))} </DropdownMenuRadioGroup> </DropdownMenuContent> </DropdownMenu> ); } ``` # Header ```ts // components/shared/header/index.tsx import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; import { ThemeToggleButton } from "./theme-toggle-button"; import { SideSheet } from "./side-sheet"; import data from "@/lib/db/data/data"; import LanguageSwitcher from "./language-switcher"; import { useLocale } from "next-intl"; export default function Header() { const locale = useLocale(); console.log("[DEBUG] Header - locale: ", locale); return ( <header className="h-16 bg-background/40 sticky top-0 border-b px-8 backdrop-blur-sm flex items-center justify-between"> {/* HEADER - 1: LOGO */} <div className="flex items-center"> <Link href={"/"}> <h1 className="font-bold text-lg md:text-xl text-nowrap">CCB Alliance</h1> </Link> </div> {/* HEADER -2: MENUS */} <div className="hidden md:flex w-full justify-end space-x-4 items-center"> {data.headerMenus.map((menu) => ( <Link key={menu.href} href={`${menu.href}`} className="text-base p-2"> {menu.name} </Link> ))} </div> <div className="hidden md:flex justify-end w-40 px-2"> {/* <LanguageSwitcher key={locale} /> */} <LanguageSwitcher /> </div> {/* HEADER -3: LOGIN */} <div className="hidden md:flex items-center gap-2 px-2"> <Link href={"/login"} className={buttonVariants({ variant: "outline" })}> Login </Link> <Link href={"/register"} className={buttonVariants({ variant: "outline" })}> Sign Up </Link> </div> <div className="w-fit flex gap-4 items-center"> <div className="md:hidden flex justify-end w-40 "> <LanguageSwitcher /> </div> <ThemeToggleButton /> <SideSheet /> </div> </header> ); } ``` Jacob (@coffsjacob) · 2025-02-24 # Notes I defined the routePaths files. ``` // lib/db/data/routes-data.ts export const routesData = { // 공개 경로 public: { static: ["/", "/blog", "/search", "/login", "/register", "/about", "/contact"], dynamic: ["/blogpost/[slug]"], }, protected: { static: ["/dashboard", "/profile", "/admin"], dynamic: ["/dashboard/settings", "/profile/[id]", "/admin/[id]"], }, }; export const publicPaths = new Set([...routesData.public.static, ...routesData.public.dynamic]); export const protectedPaths = new Set([...routesData.protected.static, ...routesData.protected.dynamic]); ``` ## Issue 1. The problem is that when I use those routesData from `i18n/routing.ts` file, then if I tried to access to a locale then try to change to another then I got an 404 not found message. 2. when I did not use pathnames, no error happened. ```ts // i18n/routing.ts import { routesData } from "@/lib/db/data/routes-data"; import { createNavigation } from "next-intl/navigation"; import { defineRouting } from "next-intl/routing"; // routesData 에서 모든 경로 추출 // const allPaths = [ // ...routesData.public.static, // ...routesData.public.dynamic, // ...routesData.protected.static, // ...routesData.protected.dynamic, // ]; // // const pathnames = Object.fromEntries(allPaths.map((path) => [path, path])); /* ROUTING CONFIGURATION * ============================= * [Step D] Routing 설정을 정의한다. * - locales: 언어 목록 * - defaultLocale: 기본 언어 * - localeCookie: 쿠키 설정 (선택) * - localePrefix: 언어별 prefix 설정 - 모드 설정 시 pathnames와 관계 없이 prefix 설정 * - pathnames: URL 경로가 다른 경우 필요 * ============================= */ export const routing = defineRouting({ locales: ["ko", "en", "vn"], defaultLocale: "ko", // localeDetection: false, // localeCookie: { name: "NEXT_LOCALE" }, localePrefix: "always", // pathnames, }); export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); ``` Alesstracker21 (@alesstracker21) · 2025-08-06 Does nobody care about this? How is it that the most popular translation framework doesnt work on THE NextJS hosting platform at all??