In the ever-expanding digital landscape, reaching a global audience is not just a goal but a necessity. Whether you're building a personal portfolio or a sophisticated web application, making it accessible to users around the world is a game-changer. And that's where the magic of internationalization comes into play!
In this blog, we'll embark on a journey to internationalize your Next.js application with app directory, unlocking the power to speak to users in their preferred languages and creating a more inclusive and user-friendly experience. Don't worry if you're new to the concept β we'll guide you through the process step by step, making it an enjoyable and enlightening adventure.
So, grab your favorite coding beverage β, put on your explorer hat π©, and let's dive into the exciting world of internationalization with Next.js! ππ
Terminology
-
Locale: An identifier for a set of language and formatting preferences. This usually includes the preferred language of the user and possibly their geographic region.
-
en-US
: English as spoken in the United States -
de-DE
: Deutsch as spoken in Germany
-
Setup
First, let's create a fresh next.js app. Make sure our versions of Next.js match. At the moment, I am using Next v14. To sync up with the package.json
file, make sure you checkout this repository created just for this tutorial.
pnpm dlx create-next-app nextjs-appdir-intl
Make sure you are using app dir because this is what we will be covering today.
Install necessary packages:
pnpm add next-intl
We are all set π
Folder structure
βββ locales
β βββ en-US.json
β βββ de-DE.json
βββ next.config.js
βββ middleware.ts
βββ i18n.ts
βββ formatters.ts
βββ app
βββ [locale]
βββ layout.tsx
βββ page.tsx
Localized routing
Let's sprinkle some magic into the user experience! Instead of making users figure out which language to choose, why not check what language their browser likes and take them straight to the right page?
Here's the plan:
-
English about page:
/en-US/about
-
German about page:
/de-DE/about
Let's edit the i18n.ts
file:
import { getRequestConfig } from "next-intl/server";
export const supportedLocales = ["en-US", "de-DE"];
export const defaultLocale = "en-US";
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./locales/${locale}.json`)).default,
}));
next.config.js
const withNextIntl = require("next-intl/plugin")("./i18n.ts");
/** @type {import('next').NextConfig} */
const nextConfig = withNextIntl({});
module.exports = nextConfig;
middleware.ts
import createMiddleware from "next-intl/middleware";
import { defaultLocale, supportedLocales } from "./i18n";
export default createMiddleware({
// A list of all locales that are supported
locales: supportedLocales,
// Used when no locale matches
defaultLocale,
});
export const config = {
// Match only internationalized pathnames
matcher: ["/", "/(de-DE|en-US)/:path*"],
};
app/[locale]/layout.tsx
import { defaultLocale, supportedLocales } from "@/i18n";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { notFound } from "next/navigation";
import { ReactNode } from "react";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
interface Props {
children: ReactNode;
params: { locale: string };
}
export default function RootLayout(props: Props) {
const { children, params } = props;
if (!supportedLocales.includes(params.locale)) notFound();
return (
<html lang={params.locale || defaultLocale}>
<body className={inter.className}>{children}</body>
</html>
);
}
It's all it takes. Now if you run the app and refresh, you should be able to see the localized path on the URL.
Add translations
Now that we have got localized routing and we can keep track of the current locale, let's go ahead and add the translations that we are gonna need.
locales/en-US.json
{
"home_page": {
"top_card": "Get started by editing app/page.tsx",
"cards": {
"card_1": {
"title": "Docs",
"description": "Find in-depth information about Next.js features and API."
},
"card_2": {
"title": "Learn",
"description": "Learn about Next.js in an interactive course with quizzes!"
},
"card_3": {
"title": "Templates",
"description": "Explore starter templates for Next.js."
},
"card_4": {
"title": "Deploy",
"description": "Instantly deploy your Next.js site to a shareable URL with Vercel."
}
}
},
"common_words": {
"by": "By"
}
}
locales/de-DE.json
{
"home_page": {
"top_card": "Beginnen Sie mit der Bearbeitung von app/page.tsx",
"cards": {
"card_1": {
"title": "Dokumente",
"description": "Hier finden Sie ausfΓΌhrliche Informationen zu den Funktionen und der API von Next.js."
},
"card_2": {
"title": "Learn",
"description": "Erfahren Sie mehr ΓΌber Next.js in einem interaktiven Kurs mit Quiz!"
},
"card_3": {
"title": "Vorlagen",
"description": "Entdecken Sie Starter-Vorlagen fΓΌr Next.js."
},
"card_4": {
"title": "Einsetzen",
"description": "Stellen Sie Ihre Next.js-Site sofort mit Vercel unter einer gemeinsam nutzbaren URL bereit."
}
}
},
"common_words": {
"by": "Von"
}
}
Translating
Now that we have all the translations, it's time to apply them in the code. We will have to replace all the hard code texts.
Translating server components
export default async function Home(props: { params: { locale: string } }) {
const t = await getTranslations({ locale: props.params.locale });
return (
<div>
<h1>{t("home_page.top_card")}</h1>
</div>
);
}
Translating client components
import { useTranslations } from "next-intl";
export default function Home(props: { params: { locale: string } }) {
const t = useTranslations();
return (
<div>
<h1>{t("home_page.top_card")}</h1>
</div>
);
}
Let's apply this into app/[locale]/page.tsx
file:
import { getTranslations } from "next-intl/server";
import Image from "next/image";
export default async function Home(props: { params: { locale: string } }) {
const t = await getTranslations({ locale: props.params.locale });
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
{t("home_page.top_card")}
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
{t("common_words.by")}{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_1.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_1.description")}
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_2.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_2.description")}
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_3.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_3.description")}
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_4.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_4.description")}
</p>
</a>
</div>
</main>
);
}
Translating Dates and Numbers
Now we have all the content localized but we still have to localize the dates and numbers. For that, we can use builtin JavaScript Intl.
formatters.ts
export class NumberFormatter {
private readonly formatter: Intl.NumberFormat;
constructor(locale: string, options?: Intl.NumberFormatOptions) {
this.formatter = new Intl.NumberFormat(locale, options);
}
format(value: number): string {
return this.formatter.format(value);
}
}
export class DateFormatter {
private readonly formatter: Intl.DateTimeFormat;
constructor(locale: string, options?: Intl.DateTimeFormatOptions) {
this.formatter = new Intl.DateTimeFormat(locale, {
month: "long",
year: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
...options,
});
}
format(value: Date): string {
return this.formatter.format(value);
}
}
This is how they have to be initialized:
const dateFormatter = new DateFormatter(props.params.locale);
const numFormatter = new NumberFormatter(props.params.locale);
Let's put them into action in the app/[locale]/page.tsx
file:
import { DateFormatter, NumberFormatter } from "@/formatters";
import { getTranslations } from "next-intl/server";
import Image from "next/image";
export default async function Home(props: { params: { locale: string } }) {
const t = await getTranslations({ locale: props.params.locale });
const dateFormatter = new DateFormatter(props.params.locale);
const numFormatter = new NumberFormatter(props.params.locale);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
{t("home_page.top_card")}
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
{t("common_words.by")}{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative gap-8 flex flex-col place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
<div>
<h2>The current time is {dateFormatter.format(new Date())}</h2>
<p>It takes {numFormatter.format(0)} to start learning</p>
</div>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_1.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_1.description")}
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_2.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_2.description")}
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_3.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_3.description")}
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{t("home_page.cards.card_4.title")}{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{t("home_page.cards.card_4.description")}
</p>
</a>
</div>
</main>
);
}
Conclusion
And that's a wrap π
Kudos to your patience for making it until the end. Hope you found it helpful, and it was a value for your time. If you have something to add or correct or got any question, please feel free to comment. That would help all of us out. Until then, see you in the next one.
References
Connect with me
Follow me on socials to consume the different types of content.
π€ Minhazur Rahman Ratul
- Twitter (@developeratul)
- Github (@developeratul)
- Instagram (@developeratul)
Top comments (1)
Great tutorial! Let me add a few tips:
For formatting dates and numbers you can use
next-intl
as wellnext-intl-docs.vercel.app/docs/usa...
next-intl-docs.vercel.app/docs/usa...
Also, I can recommend a free tool Gitloc, that can help you to create translation files, as easy as possible.