Internationalisation (i18n) in Next.js with next-intl
Launching a web application in multiple languages can seem daunting, but tools like next-intl make this process much easier. Having recently used next-intl in a bilingual Next.js project, I've seen firsthand how it simplifies adding internationalisation features to projects built with Next.js.
This article is not a tutorial; instead, it serves as a companion to next-intl's documentation, providing an introductory look into how next-intl helps implement necessary i18n features.
Note: This article is intended for individuals looking to use next-intl in a Next.js project with the App Router. It also assumes some basic knowledge about Next.js. If you want to learn more about Next.js, you can refer to their documentation.
How to get started with next-intl
If you want to use next-intl in your Next.js project, you can follow the basic set up instructions in the documentation. Once finished, the file structure of your Next.js project should look a bit like this:
├── messages (1)
│ ├── en.json
│ └── ...
├── next.config.mjs (2)
└── src
├── i18n.ts (3)
├── middleware.ts (4)
└── app
└── [locale]
├── layout.tsx (5)
└── page.tsx (6)
You'll notice that all files inside /app
are nested under /app/[locale]
. This is required by Next.js so that the router can dynamically handle different locales in the route and forward the locale
parameter to every layout and page.
en-GB
for English in the United Kingdom, en-US
for English in the United States, es
for Spanish without a specific region.My top recommendation is to implement basic internationalisation (i18n) in your project as soon as possible. This not only shapes your project's structure from the beginning but also streamlines the development process by integrating i18n into your architectural decisions early on. This approach helps you avoid costly and time-consuming rework that can occur if internationalisation is an afterthought. By setting up next-intl early, you'll gain a deep understanding of how internationalisation works within Next.js, guiding you to make informed decisions as your project progresses.
Routing and locale detection
In internationalisation, routing is important because it helps to organise URLs by different locales.
There are two ways to internationalise routes:
Prefix-based routing: All routes have a prefix with a locale identifier (e.g.,
mywebsite.com/en/services
,mywebsite.com/es/services
).Domain-based routing: Localised content is provided from distinct domains or subdomains (e.g.,
mywebsite.com/services
andmisitioweb.es/services
have separate domains).
The configuration of your middleware.ts
file is critical and varies depending on the chosen routing strategy. The middleware detects the user's locale and redirects appropriately to display content in their preferred language. Once a locale is detected, next-intl saves it in the NEXT_LOCALE
cookie for future reference.
For detailed instructions on configuring your middleware according to your routing strategy, please refer to this part of the official next-intl documentation.
Navigation
next-intl offers a replacement for the built-in navigation features from Next.js. This automatically manages all i18n routing for you and it's very simple to set up.
First, you need to choose the pathname strategy for your app:
Shared pathnames: same pathname for all locales (e.g.,
/en/services
and/es/services
)Localised pathnames: distinct pathnames for each locale (e.g.,
/en/services
and/es/servicios
)
Next, create a navigation.ts
file at the root of your project. Follow these instructions to set up this file based on your chosen pathname strategy.
For example, if your app uses shared pathnames, your navigation.ts
file might look like this:
// navigation.ts
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
export const locales = ['en', 'de'] as const; // this can be imported from elsewhere
export const localePrefix = 'always'; // Default
export const {Link, redirect, usePathname, useRouter} =
createSharedPathnamesNavigation({locales, localePrefix});
Then, you will import Link
, useRouter
, usePathname
and redirect
directly from this navigation.ts
file, rather than from Next.js. If you're collaborating on a project, make sure that everyone knows that they should import navigation elements from here. This guarantees that all language-specific routing is automatically managed by next-intl with no unexpected behaviour.
// An example of using next-intl navigation elements for shared pathnames
import { Link } from '@/navigation';
// When the user is on `/en`, the link will point to `/en/services`
<Link href="/services">Services</Link>
// You can override the `locale` to switch to another language
<Link href="/" locale="es">Switch to Spanish</Link>
Note: for localised pathnames, check how to correctly use next-intl
navigation APIs, as they sometimes differ a bit from the standard Next.js behaviour. Please refer to this part of the next-intl documentation.
Messages
One of the primary challenges in building internationalised websites is configuring content in multiple languages. next-intl simplifies this by allowing you to manage all language-specific strings, known as "messages," in separate JSON files within a /messages
directory.
You can organise these strings under different namespaces to keep related text grouped together. Here's how you might structure these files (in this example, we have two namespaces "Home" and "About"):
// en.json
{
"Home": {
"welcome": "Welcome to our website",
"subtitle": "Explore our services and latest updates"
},
"About": {
"title": "About us",
"description": "Learn more about our mission, vision, and values"
},
}
// es.json
{
"Home": {
"welcome": "Bienvenido a nuestro sitio web"
"subtitle": "Explora nuestros servicios y las últimas actualizaciones"
},
"About": {
"title": "Sobre nosotros"
"description": "Conoce más sobre nuestra misión, visión y valores"
},
}
Messages can be rendered using the useTranslations
hook from next-intl
. For example, this is how we would display the title from the "About" namespace:
import { useTranslations } from 'next-intl';
function About() {
// Here, we are loading the messages from the About namespace
const t = useTranslations('About');
return <h1>{t('title')}</h1>;
}
Messages can also adapt to dynamic content. For example, personalising a greeting with a user's name:
// en.json
"message": "Hello {name}"
// es.json
"message": "Hola {name}"
// this code would be inside of a component
t('message', { name: 'Lucy' }) // Hello Lucy | Hola Lucy
next-intl uses ICU message syntax, meaning that you can express all sorts of language nuances from within your JSON files. For example, managing plurals with the plural
argument:
// en.json
"message": "{count, plural, =0 {You have no messages yet} =1 {You have 1 message} other {You have # messages}}"
// es.json
"message": "{count, plural, =0 {No tienes mensajes todavía} =1 {Tienes 1 mensaje} other {Tienes # mensajes}}"
t('message', {count: 5}) // You have 5 messages | Tienes 5 mensajes
You can also use the select
argument to accommodate languages with gender-specific expressions. For example, you might adjust messages based on the user's gender:
"message": "{gender, select, female {Bienvenida} male {Bienvenido} other {Bienvenidx}} {name}"
t('message', {gender: 'female', name: 'Lucy'}) // Bienvenida Lucy
If you'd like to dive deeper into message configuration, check out this part of the next-intl documentation.
Rendering messages
next-intl recommends handling internationalisation on the server side for better performance. This way, messages stay on the server and don't require serialisation for the client side.
Rendering messages in server components is simple. You can choose to load all messages or the messages of a single namespace:
import { useTranslations } from 'next-intl';
function ComponentOne() {
// Loads all messages from the "Welcome" namespace
const t = useTranslations('Welcome');
// We don't have to repeat the namespace here
return <h1>{t('greeting')}</h1>;
}
function ComponentTwo() {
// Here, we don't specify a namespace
const t = useTranslations();
return (
<>
// So, instead we specify the namespace before the key
<h1>{t('Welcome.greeting')}</h1>
<p>{t('About.title')}</p>
</>
)
}
next-intl's translation functions can be called within any server component at any level of the component tree without affecting performance. This means you don't have to unnecessarily pass props down, and you can handle string translations directly in each server component.
Client components
However, modern web applications often include interactive features and, as a result, client components. To maintain optimal performance, next-intl doesn't automatically provide messages to client components. But there are various ways to handle the translation of client components:
Passing translated strings as props:
Passing server-rendered translations to client components:/* Server component (Greeting) passing translations to a client component (InteractiveButton) */ import { useTranslations } from 'next-intl'; import InteractiveButton from './InteractiveButton'; function Greeting() { const t = useTranslations('Greeting'); return <InteractiveButton label={t('hello')} />; }
Moving state to server side:
Handle dynamic state such as pagination on the server side using page params, search params, cookies or database state.Using NextIntlClientProvider to load some messages
In scenarios where client-specific translations are necessary, wrap components inNextIntlClientProvider
and provide the relevant messages from chosen namespaces:import pick from 'lodash/pick'; import {NextIntlClientProvider, useMessages} from 'next-intl'; import ClientCounter from './ClientCounter'; export default function Counter() { // Receive messages provided in `i18n.ts` … const messages = useMessages(); return ( <NextIntlClientProvider messages={ // … and provide the relevant messages pick(messages, 'ClientCounter') } > <ClientCounter /> </NextIntlClientProvider> ); }
Using NextIntlClientProvider to load all messages
In the below example, the entire application is wrapped withNextIntlClientProvider
to allow all client components to have access to all messages:import {NextIntlClientProvider, useMessages} from 'next-intl'; import {notFound} from 'next/navigation'; export default function LocaleLayout({children, params: {locale}}) { // ... // Receive messages provided in `i18n.ts` const messages = useMessages(); return ( <html lang={locale}> <body> <NextIntlClientProvider locale={locale} messages={messages}> {children} </NextIntlClientProvider> </body> </html> ); }
The method you choose depends on how interactive your app is. If a part of your app is highly interactive, like a client dashboard, it's probably best to pass messages using NextIntlClientProvider
. This allows you to make full use of all the features of next-intl within those components.
Asynchronous components
For dynamic scenarios involving data fetching, next-intl provides awaitable versions of their functions to handle translations in asynchronous components. For example, here we use getTranslations
instead of useTranslations
:
import {getTranslations} from 'next-intl/server';
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getTranslations('ProfilePage');
return (
<PageLayout title={t('title', {username: user.name})}>
<UserDetails user={user} />
</PageLayout>
);
}
Opting out of dynamic rendering
When you use functions like useTranslations
in server components, your pages will automatically opt into dynamic rendering.
This might not be the behaviour that you expect or want. For example, you might have a blog and want the content to be statically rendered.
To address this, next-intl have come up with a temporary API that can be used to distribute the locale that is received via params
in layouts and pages for usage in all server components that are rendered as part of the request.
You'll need to include unstable_setRequestLocale
to all layout.ts
and page.
files where you'd like to opt into static rendering:
import {unstable_setRequestLocale} from 'next-intl/server';
export default async function Blog({children, params: {locale}}) {
unstable_setRequestLocale(locale);
return (
// ...
);
}
This is a step that’s easy to forget but can lead to unexpected behaviour and bugs if forgotten.
While the label "unstable" may suggest caution, this API is stable in terms of functionality and reliability when used as directed. It's named in this way because it's supposed to act as a stopgap solution until Next.js potentially introduces a native API for accessing parts of the URL related to locales. You can read more about it here.
Conclusion
I hope you found this article interesting and that it provided a helpful insight into using next-intl
for internationalisation in Next.js applications. My goal was to demonstrate some key features and show how they can improve the management of multilingual content in a simple and effective way.
I highly recommend diving into the official next-intl
documentation for a more comprehensive understanding of its capabilities. The documentation is thorough and well-written, covering many features that I haven't touched upon.
It's also worth noting that the next-intl community is very active and supportive. I've personally had a great experience getting timely answers to my questions on GitHub, which has made implementing and troubleshooting much smoother.
If you've already experimented with next-intl, I'd love to hear about your experiences. Please share your thoughts and any insights in the comments below. Your feedback not only helps others but also enriches the community's collective knowledge.
Thank you for reading, and happy coding!
Further reading
Next.js internationalization (i18n): Going international with next-intl: deep dive tutorial written by the library's author Jan Amann
Internationalization In Next.js 13 With React Server Components: shorter tutorial written by Jan Amann
Simplifying Next.js Authentication and Internationalization with Next-Auth and Next-Intl: blog post about how to effectively integrate next-intl and next-auth middleware.
next-intl's GitHub repo: don't forget to give it a star ⭐️
Design principles of next-intl: a deep dive into the design principles of next-intl – not essential reading but certainly very interesting.
Special thanks
Thank you to Jan Amann for creating this incredible library and for also taking the time to review this blog post before posting.
Subscribe to my newsletter
Read articles from Lucy Macartney directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Lucy Macartney
Lucy Macartney
I'm a software developer and technical writer from London UK, living in Cali Colombia.