Localization in Nextjs with App Router

Localization is a very important aspect of building an application, especially when you have users with different languages in a new target market. The goal of localization is to break down the communication barriers making content more accessible to everyone. It’s not something we think about but it’s very important.
In this article, we will be looking at how to set up localization in nextjs application with the app router.

What we will cover in this post

  • Create a new nextjs application
  • Install the 118next package
  • Write translation files
  • Configure the next-intl package
  • Tailor our internationalization library according to our needs

For the purposes of the post, we will be building out a basic e-commerce application. To see a live working version of this blog post, check out the demo-app over here. You can also access to the complete source code on github here too.

Create a new nextjs application

I prefer to use pnpm but you can any package manager of your choice.

pnpm create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

Install the next-intl library

pnpm install next-intl
Enter fullscreen mode Exit fullscreen mode

Create Messages file

After the installation, the first step is to create a messages folder at the root of your application
You can save the messages locally or fetch them from a remote source depending on your workflow.

At the root of your project, create a messages folder where you can create JSON files for each locale like below.

{ "Index": { "title": "Hello world!" }
}
Enter fullscreen mode Exit fullscreen mode

Configure Plugin

The next step is to configure the createNextIntlPlugin plugin from the next-intl package in your next.config files. This plugin will provide the i18n configuration to the server components as follows:

import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); /** @type {import('next').NextConfig} */
const nextConfig = {}; export default withNextIntl(nextConfig);
Enter fullscreen mode Exit fullscreen mode

Create i18n.ts file to setup the configuration

After this, create a i18n.ts file. This is to create a request-scoped configuration that can be used to provide messages based on the user’s locale in the server components

import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server"; const locales = ["en", "de"]; export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale)) notFound(); return { messages: (await import(`../messages/${locale}.json`)).default, };
}); 
Enter fullscreen mode Exit fullscreen mode

Create a middleware to handle requests

Middleware is used to determine the locale for each request and handle redirects accordingly. In this step, you’ll list all the supported locales for your application and match them with the pathnames. You can also set a default locale so that incoming requests automatically default to it if no specific locale is specified.

import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ locales: ['en', 'de'], defaultLocale: 'en'
}); export const config = { matcher: ['/', '/(de|en)/:path*']
};
Enter fullscreen mode Exit fullscreen mode

This code sets up middleware that supports English and German, with English as the default locale. It will match the specified paths and handle locale-based routing for your application.

Create a locale in the app/[locale]/layout.tsx file

Since we have already set up the middleware with the respective locales, we can retrieve the matched locale from the params and use it to configure the page language in the layout.tsx file. We will then pass the messages to the NextIntlClientProvider.

import { AbstractIntlMessages, NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { Header } from "./Header";
import ProductCard from "./Card";
import { HeroSection } from "./HeroSection"; export default async function LocaleLayout({ children, params: { locale },
}: { children: React.ReactNode; params: { locale: string };
}) { const messages = await getMessages(); return ( <html lang={locale}> <body> <NextIntlClientProvider messages={JSON.stringify(messages) as unknown as AbstractIntlMessages} > <div className='max-w-6xl mx-auto p-12'> <Header /> </div> <HeroSection /> <main className='max-w-6xl mx-auto p-12'>{children}</main> </NextIntlClientProvider> </body> </html> );
}
Enter fullscreen mode Exit fullscreen mode

Your project structure should look like this

├── public/
├── messages/
│ ├── en.json
│ └── de.json
├── src/
│ ├── config.ts
│ ├── i18n.ts
│ ├── middleware.ts
│ ├── navigation.ts
│ ├── app/
│ │ └── [locale]/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── components/
│ └── pages/
│ ├── index.js
│ └── about.js
├── package.json
└── README.md

Rendering i18n messages with the useTranslations hook

It is now time to render the 118next messages based on the user settings in the UI. next-intl provides a useTranslations hook used to render the messages. The hook takes in a namespace or a key based on the structure of your "language.json" file.
To illustrate let’s integrate the translation capabilities into our product app starting from the hero HeroSection. In our application we have

 "Hero": { "title": "Der beste Online-Shop der Welt für Laptops und Macbooks", "ctaButton": "Jetzt kaufen" },
Enter fullscreen mode Exit fullscreen mode
import { useTranslations } from "next-intl"; export const HeroSection = () => { const t = useTranslations("Hero"); return ( <section className='Hero-banner flex items-center justify-center lg:p-8 p-4'> <div className='flex flex-col'> <h1 className='lg:text-3xl text-lg text-white text-center'> {t("title")} </h1> <div className='flex items-center justify-center'> <button className='bg-black text-white px-6 py-2 w-fit my-8'> Shop Now </button> </div> </div> </section> );
};
Enter fullscreen mode Exit fullscreen mode

It uses the key to retrieve the corresponding messages.
Now when you check your browser on http://localhost:3000/en, it shows the English version of the translations while http://localhost:3000/de shows the German version like below
(Translation Example)

Interpolation of dynamic values

This is a technique that can be used to insert dynamic values into a prefixed text.

"message": "Hello {name}!"
Enter fullscreen mode Exit fullscreen mode

We can replace the {name} with a dynamic value

t('message', {name: 'Albert'});
Enter fullscreen mode Exit fullscreen mode

resulting in

"Hello Albert"
Enter fullscreen mode Exit fullscreen mode

next-intl also supports formatting rich texts with custom tags

{ "message": "Please refer to <guidelines>the guidelines</guidelines>."
}
Enter fullscreen mode Exit fullscreen mode
t.rich('message', { guidelines: (chunks) => <a href="/guidelines">{chunks}</a>
});
Enter fullscreen mode Exit fullscreen mode

To render an array of messages, we can map over the keys to the corresponding messages like in our little e-commerce application

 "data": { "product1": { "title": "Macbook Pro", "price": 1200, "image": "/products/macbook-pro.jpg", "description": "Work on anything, anywhere with the incredibly light and speedy Macbook Air 2020. The M1 chip is a game-changer. It's 3.5x faster than the previous Macbook Air, and packs in 8 CPU and 7 GPU cores so you can take on video-editing and gaming. Plus, it's incredibly power-efficient. The M1 lets you browse for up to 15 hours, or watch Apple TV for around 18 - that's a full flight from London to Sydney!" }, ...others }
Enter fullscreen mode Exit fullscreen mode

The recommended approach to render the product cards is to map over the keys like this

import {useTranslations, useMessages} from 'next-intl'; function ProductList() { const t = useTranslations('Products'); const messages = useMessages(); const keys = Object.keys(messages.Products.data); return ( <ul> {keys.map((key) => ( <li key={key}> <h2>{t(`${key}.title`)}</h2> <p>{t(`${key}.description`)}</p> </li> ))} </ul> );
}
Enter fullscreen mode Exit fullscreen mode

We need to render each product card so we can use Object.values like this

export default function Index() { const t = useTranslations("Product"); const messages: any = useMessages(); const products = Object.values(messages.Product.data) as unknown as Product[]; return ( <div> <div className='mt-4'> <div className='mb-4'> <h1>{t("title")}</h1> </div> <ul className='grid lg:grid-cols-3 grid-cols-1 gap-9'> {products.map((product: Product, i: number) => ( <li key={i}> <ProductCard buttonText={t("productCardMeta.buttonText")} key={i} product={product} /> </li> ))} </ul> </div> </div> );
}
Enter fullscreen mode Exit fullscreen mode

This should work as expected

Formatting Numbers

You can also format a number within a message

{ "price": "This product costs {price, number, currency}"
}
Enter fullscreen mode Exit fullscreen mode
t( 'price', {price: 32000.99}, { number: { currency: { style: 'currency', currency: 'EUR' } } }
);
Enter fullscreen mode Exit fullscreen mode

Implementing language switching in Next.js App Router using next-intl

next-intl simplifies language switching in Next.js by automatically handling locale information within standard navigation APIs.
By employing shared pathnames, you can directly map Next.js routes to user-requested URLs without additional complexity.
With this configuration, you gain access to routing components and methods like Link and usePathname, enabling intuitive navigation within your Next.js project.
Create a navigation.ts in your src folder and add the following:

import { createSharedPathnamesNavigation } from "next-intl/navigation";
import { locales } from "./config"; export const { Link, redirect, usePathname, useRouter } = createSharedPathnamesNavigation({ locales }); 
Enter fullscreen mode Exit fullscreen mode

To implement a language switch feature in your Navbar or any desired page, you can attach the pathname to the href property along with a locale. Here’s an example of a Header.tsx component:

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation"; import { locales } from "~/config";
import { LocalLink } from "./LocalLink"; type LocaleItem = "en" | "de"; const NAV_LINKS = ["Shop", "Cart"]; export const Header = () => { const [selected, setSelected] = useState<LocaleItem>(); const pathname = usePathname(); const localePath = pathname.split("/")[1]; const handleChangeLocale = (item: LocaleItem) => { setSelected(item); }; useEffect(() => { setSelected(localePath as LocaleItem); }, [localePath]); return ( <header className='flex items-center justify-between'> <h1>Tech Shop</h1> <ul className='flex items-center gap-2'> {NAV_LINKS.map((nav) => ( <li key={nav}> <Link href='#'>{nav}</Link> </li> ))} </ul> <div className='relative flex items-center justify-center rounded-full'> <ul className='bg-white flex'> {locales.map((locale, i) => ( <li key={i} onClick={() => handleChangeLocale(locale as LocaleItem)} className={`border-l border-t border-b last:border-r ${ selected === locale ? "bg-gray-100" : "" }`} > <LocalLink locale={locale} className={`flex p-4 items-center gap-2`} > <Image src={`/icons/${locale}.svg`} alt='' height={20} width={20} className='rounded-full' /> <h4 className='uppercase text-sm'>{locale}</h4> </LocalLink> </li> ))} </ul> </div> </header> );
};
Enter fullscreen mode Exit fullscreen mode

Wrapping up

I hope this post was helpful to you. You can find the complete source code on github and the demo-app over here.
You can read more on the official documentation site

The full guide is published here on my personal website 😉

References


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top