Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Localization in Nextjs with App Router
Albert
Albert

Posted on

     

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 thenext-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 thedemo-app over here. You can also access to thecomplete source code on github here too.

Create a new nextjs application

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

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

Install thenext-intl library

pnpm install next-intl
Enter fullscreen modeExit 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 modeExit fullscreen mode

Configure Plugin

The next step is to configure thecreateNextIntlPlugin plugin from thenext-intl package in yournext.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 modeExit fullscreen mode

Createi18n.ts file to setup the configuration

After this, create ai18n.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 modeExit 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 modeExit 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 theapp/[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 modeExit 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 theuseTranslations hook

It is now time to render the 118next messages based on the user settings in the UI.next-intl provides auseTranslations hook used to render the messages. The hook takes in anamespace or akey 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 modeExit 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 modeExit fullscreen mode

It uses thekey to retrieve the corresponding messages.
Now when you check your browser onhttp://localhost:3000/en, it shows the English version of the translations whilehttp://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 modeExit fullscreen mode

We can replace the{name} with a dynamic value

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

resulting in

"Hello Albert"
Enter fullscreen modeExit fullscreen mode

next-intl also supports formatting rich texts with custom tags

{  "message": "Please refer to <guidelines>the guidelines</guidelines>."}
Enter fullscreen modeExit fullscreen mode
t.rich('message', {  guidelines: (chunks) => <a href="/guidelines">{chunks}</a>});
Enter fullscreen modeExit 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 modeExit 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 modeExit fullscreen mode

We need to render each product card so we can useObject.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 modeExit 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 modeExit fullscreen mode
t(  'price',  {price: 32000.99},  {    number: {      currency: {        style: 'currency',        currency: 'EUR'      }    }  });
Enter fullscreen modeExit 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 anavigation.ts in yoursrc 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 modeExit 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 aHeader.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>

  &lt;ul className='flex items-center gap-2'&gt;    {NAV_LINKS.map((nav) =&gt; (      &lt;li key={nav}&gt;        &lt;Link href='#'&gt;{nav}&lt;/Link&gt;      &lt;/li&gt;    ))}  &lt;/ul&gt;  &lt;div className='relative flex items-center justify-center rounded-full'&gt;    &lt;ul className='bg-white flex'&gt;      {locales.map((locale, i) =&gt; (        &lt;li          key={i}          onClick={() =&gt; handleChangeLocale(locale as LocaleItem)}          className={`border-l border-t border-b last:border-r ${            selected === locale ? "bg-gray-100" : ""          }`}        &gt;          &lt;LocalLink            locale={locale}            className={`flex p-4 items-center gap-2`}          &gt;            &lt;Image              src={`/icons/${locale}.svg`}              alt=''              height={20}              width={20}              className='rounded-full'            /&gt;            &lt;h4 className='uppercase text-sm'&gt;{locale}&lt;/h4&gt;          &lt;/LocalLink&gt;        &lt;/li&gt;      ))}    &lt;/ul&gt;  &lt;/div&gt;&lt;/header&gt;
Enter fullscreen modeExit fullscreen mode

);
};

Enter fullscreen modeExit fullscreen mode




Wrapping up

I hope this post was helpful to you. You can find thecomplete source code on github and thedemo-app over here.
You can read more on theofficial documentation site

The full guide is published here on mypersonal website 😉

References

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

A software engineer
  • Location
    Ghana, Accra
  • Education
    University of Professional Studies
  • Work
    Web developer
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp