Shane Chaffe
3 min read ⏳
Understanding how localisation works with Next.js Pages Router
Typically most people will go with using i18n as it is built into Next.js, when using that it is key to understand that the browser has headers that are typically used to infer what a users locale is.
Here is a general example:
i18n: {
locales: ['default', 'en-GB', 'en-US', 'zh-Hant-TW', 'fr-FR', 'de-DE', 'ja-JP', 'ko-KR'],
defaultLocale: 'en-GB',
// if this is false, the middleware will fall back to a locale that is either in the headers of the browser or cookies
localeDetection: false
},
Here is more information on the header that is used in the middleware: Accept-Language Header
So, because my default locale is ‘en-GB’ the header is able to know that my site should serve that content, the same would be true for the other locales I have in my list of locales.
When visiting a live site, we can see that the request to the page automatically does the job for us, it knows based on my browser, to default to en-GB or fallback to en-US. In my case, the other way around would be true as en-GB is my default.
With that being said though, Next.js doesn’t append the locale to the URL if it is indeed the default locale, so you’d need to change the configuration to as follows:
middleware.ts:
import { NextRequest, NextResponse } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/;
export async function middleware(req: NextRequest) {
// Skip Next.js internal requests and public files
if (
req.nextUrl.pathname.startsWith('/_next') ||
req.nextUrl.pathname.includes('/api/') ||
PUBLIC_FILE.test(req.nextUrl.pathname)
) {
return;
}
// if locale is missing or 'default', redirect to the correct locale
if (!req.nextUrl.locale || req.nextUrl.locale === 'default') {
const locale = req.cookies.get('NEXT_LOCALE')?.value || 'en-GB'; // Fallback to 'en-GB' if no cookie is set
// Redirect to the correct locale
return NextResponse.redirect(
new URL(`/${locale}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url)
);
}
}
next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.ctfassets.net",
pathname: "/**"
}
]
},
i18n: {
locales: ['default', 'en-GB', 'en-US', 'zh-Hant-TW', 'fr-FR', 'de-DE', 'ja-JP', 'ko-KR'],
defaultLocale: 'default',
// if this is false, the middleware will fall back to a locale that is either in the headers of the browser or cookies and defined in the accepted list above
localeDetection: true
},
trailingSlash: false
};
export default nextConfig;
With these changes, the locale will always be “default” so it will automatically change to the one that is best suited to you, i.e. en-GB. That does mean at build team if using SSG for example and you need to fetch data from a CMS like Contentful, requests will be made to homepage.com/default which isn't going to work. You need to handle this case at the time the build happens in getStaticPaths.
The way to handle this is to reassign the locale as it becomes available:
export async function getStaticProps({ locale }) {
if (locale === "default") {
locale = "en-GB";
}
const landingPage = await client.getEntries({
content_type: "page",
include: 5,
"fields.slug[match]": "/",
locale,
});
const landingPageProps = landingPage.items[0];
return {
props: {
landingPageProps,
},
};
}
The only gotchya to this is that you need to do this every time you use getStaticProps otherwise when your project attempts to build, a request will be made to "default" - which doesn't exist. Now that we've handled that, we can discuss how this works with Contentful.
Now how do you defer between the response of Contentful to decide what data to serve?
Next.js allows us to extract the locale in getStaticProps and here is an example of that:
export async function getStaticProps({ locale, params }) {
const { reviewId } = params;
const reviewPage = await client.getEntries({
content_type: "article",
include: 5,
"fields.slug[match]": reviewId,
locale,
});
if (!reviewPage.items.length) {
return { notFound: true };
}
const reviewPageProps = reviewPage.items[0];
return {
props: {
reviewPageProps,
},
};
}
Just to walk through the above snippet line by line, getStaticProps has access to the context of the app so we can destructure and take out locale and params. This allows us to get the current locale AND access to the slug at build time for getStaticProps. Then we can make use of the Contentful JS SDK and pass these values to the getEntries call to retrieve the entry we want with the keys that we use above, thanks to this the SDK knows how to filter between which locale it should resolve, please ensure that locale is available and configured in Contentful.
Tip: Use getEntries so you can resolve data that is nested deeper than you might think as getEntry only resolves up until a certain level.
The call will look something like this under the hood:
https://preview.contentful.com/spaces/<spaceId>/environments/<envId>/entries/<entryId>?locale=<localeHere>
Then with the response of the data you can continue to consume the props as you normally would in your application.
One thing to remember is to ensure you update the language of your HTML document as you change between locales for your users since we're using automatic detection here. I was able to achieve this in the _document.js file like so:
import { Html, Head, Main, NextScript } from "next/document";
export default function Document(props) {
// This is responsible for actually updating the HTML lang in the html
const currentLocale = props.__NEXT_DATA__.locale || "en-GB";
return (
<Html lang={currentLocale} >
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}
So thanks to this, if we change the locale to any available locale our application serves, the document language will also update to remain relevant and stay true to serving localised content.
Next.js doesn't know about variants of a page so it's up to you to add the hreflang
meta tags using next/head
. You can learn more about hreflang
it in the Google Webmasters documentation.
If you're curious to know where this is implemented, you can take a look at the Coffee Reviews website which is a victim born out of curiosity.
Technology used: