Persisting a user's language choice
Using a cookie to remember a selection
Problem
A user manually selects 'French' on a site. When they close their browser and later type in the site's main address (e.g., example.com), the application reverts to its default (e.g., English) or auto-detected language. This failure to remember their choice forces the user to find the language switcher and re-select their language every time they start a new session.
Solution
When a user selects a language, store that choice in a cookie. In the middleware, when a user visits the root path (/), check for this cookie before checking the Accept-Language header. If a valid cookie is found, redirect the user to their chosen language's root (e.g., /fr), overriding any browser default.
Steps
1. Define language configuration
Create a central configuration file to store your supported languages, a default language, and the name of the cookie you will use to store the user's preference.
// i18n.config.ts
export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';
export const localeCookieName = 'NEXT_LOCALE';
2. Install a language parser
You still need a parser for the Accept-Language header, which will be used as a fallback if no cookie is set.
npm install accept-language-parser
3. Create the middleware
Create a middleware.ts file at the root of your project. This middleware will check for the NEXT_LOCALE cookie first. If it's not found, it will fall back to checking the Accept-Language header. This logic will only apply to requests for the root path (/).
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import parser from 'accept-language-parser';
import {
locales,
defaultLocale,
localeCookieName,
} from './i18n.config';
function getPreferredLocale(request: NextRequest) {
// 1. Check for the cookie
const cookie = request.cookies.get(localeCookieName);
if (cookie) {
const locale = cookie.value;
if (locales.includes(locale)) {
return locale;
}
}
// 2. Check the Accept-Language header
const acceptLang = request.headers.get('Accept-Language');
if (acceptLang) {
const bestMatch = parser.pick(locales, acceptLang, {
loose: true,
});
if (bestMatch) {
return bestMatch;
}
}
// 3. Return the default
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Check if the request is for the root path
if (pathname === '/') {
// Get the user's preferred language (from cookie or header)
const bestLocale = getPreferredLocale(request);
// Redirect to the best-matched language path
request.nextUrl.pathname = `/${bestLocale}`;
return NextResponse.redirect(request.nextUrl);
}
// 2. For all other paths, continue as normal
return NextResponse.next();
}
export const config = {
matcher: [
// We only want to run this on the root path for now
'/',
// We also need to match non-root paths to let them pass
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
This middleware's logic now correctly prioritizes the user's explicit choice (the cookie) over their implicit preference (the browser header) when they visit the site's root.