Creating a next.js middleware to better handling localization
Localization can be hard to handle in your web sites. Luckily for us there are more tools everyday. While working with next.js I ended up with a problem with the default way they handle it as they only apply localization changes on the root page, so if you navigate to a subpath you will not be able to see the page in the correct locale. After doing some spikes I ended up creating a middleware that handles this using the browser configuration.
In this post we will see what is a middleware for next.js and how you can solve this issue with it.
Next.js middleware
First, let me set some context. I will talk about middleware's in Next.js 12.3 as it has some differences from older versions and it surely will have differences in the future.
Now, what is a middleware? Middleware allows you to execute code before the request is processed. Based on the request information you can perform different actions which is a super useful tool for lots of scenarios such a authentication, A/B Testing, localized pages and more.
The basics are simple. Just create a middleware.ts
file in the root of your project with the following stucture and it will be call on every request.
import { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
console.log(req.nextUrl);
return undefined;
}
As this will be call on everything, you might want to apply the logic only to a certain patters for that you can analyze the pathname of the request. For example you could ignore all the request that target the _next
folder or the api
and also files as you can see in the following example.
import { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
const shouldHandleLocale = !(
pathname.startsWith("/_next") ||
pathname.startsWith("/api/") ||
pathname.includes(".", pathname.lastIndexOf("/"))
);
if (shouldHandleLocale) {
console.log(req.nextUrl);
}
return undefined;
}
Handling locales
First, let's get the user's default locale. To get this, we will take advantage of the accept-language
header which looks like this: en-US,en;q=0.9,es;q=0.8
. We will need to parse it and check if we are supporting those locales. The following code does that, ordering the locales by priority and then filtering them.
const LOCALES = ["es", "en"];
const getBrowserLanguage = (req: NextRequest) => {
return req.headers
.get("accept-language")
?.split(",")
.map((i) => i.split(";"))
?.reduce(
(ac: { code: string; priority: string }[], lang) => [
...ac,
{ code: lang[0], priority: lang[1] },
],
[]
)
?.sort((a, b) => (a.priority > b.priority ? -1 : 1))
?.find((i) => LOCALES.includes(i.code.substring(0, 2)))
?.code?.substring(0, 2);
};
Now, for the following approach, you will need to define a default
locale to detect that we need to process it and then the other locales that you are supporting, so in your configuration, set the following.
"defaultLocale": "default",
"locales": ["default", "en", "es"],
Then, we will update our middleware to redirect to the detected locale if the locale was the default one. For doing this, we need to clone the url, and update the locale with the language that we detect. It's important to have a fallback as the headers approach could fail depending on the client's browser. Finally, we will perform a call to NextResponse.redirect(url);
with the updated url.
import { NextRequest, NextResponse } from "next/server";
const DEFAULT_LANGUAGE = "es";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
const shouldHandleLocale = !(
pathname.startsWith("/_next") ||
pathname.startsWith("/api/") ||
pathname.includes(".", pathname.lastIndexOf("/"))
);
if (shouldHandleLocale && req.nextUrl.locale === "default") {
const url = req.nextUrl.clone();
const language =
req.cookies["NEXT_LOCALE"] ?? getBrowserLanguage(req) ?? DEFAULT_LANGUAGE;
url.locale = language;
return NextResponse.redirect(url);
}
return undefined;
}
Final thoughts
This approach works fine for most of the scenarios but you might even need more things to consider. For example, in my case I had to check that the request could be on an iframe and apply a similar approach to keep the same locale as in the parent window, for that you could play around with the sec-fetch-dest
header to see if the request is running on an iframe and also the referer
header to see the parent's url. Additionally, in some cases I ended up with double locale in the path (i.e., /en/es/
) so I had to add some logic to correct those navigations.
Middlewares are a great tool to handling this kind of cases but you need to consider that the logic you add in there will be executed for everything which might add some performance issues.