
How to Build Multilingual Website with Next.js + Next-intl
There are many tools available today for creating multilingual websites.
Browser translation can easily convert any page, but as a programmer,
I want to understand exactly how to implement this myself to improve my website's appearance and user experience.
So, I started browsing the internet and discovered an interesting solution: combining Next.js and next-intl
to achieve this functionality.
Here’s how I approached it: I created a new project to get things started.
npx create-next-app@latest multilingual-website
Then, I changed the directory to the newly created project and installed the required next-intl package:
cd multilingual-website
npm install next-intl
I’m assuming you’re using the App Router, so we’ll follow this recommended file structure:
├── messages
│ ├── en.json
│ ├── id.json
│ └── ...
├── next.config.ts
└── src
├── i18n
│ ├── routing.ts
│ ├── navigation.ts
│ └── request.ts
├── middleware.ts
└── app
└── [locale]
├── layout.tsx
└── page.tsx
To support unique pathnames for each language your app offers, next-intl allows for two routing strategies:
- Prefix-based routing (e.g. /en/about)
- Domain-based routing (e.g. en.example.com/about)
Let’s begin setting up the files, and I’ll explain each of them clearly as we go.
1. messages/en.json
This file contains the translations specific to a language. You can create additional files like id.json
or fr.json
depending on the languages you want to support.
messages/en.json
{
"HomePage": {
"title": "Hello, I'm Ayi Akbar",
"description": "The best way to learn is by doing."
}
}
2. next.config.ts
We need to configure a plugin to inject request-specific i18n settings (like your messages) into Server Components.
next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const nextConfig: NextConfig = {
/* config options here */
};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);
3. src/i18n/routing.ts
You’ll need to integrate i18n into two main areas of your app:
- Middleware: for negotiating the locale and managing redirects (e.g. / → /en)
- Navigation APIs: lightweight wrappers around components like <Link />
So, to centralize the routing configuration, create routing.ts like this:
src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
// A list of all locales that are supported
locales: ["en", "id"],
// Used when no locale matches
defaultLocale: "en",
});
4. src/i18n/navigation.ts
Once we have our routing configuration in place, we can use it to set up the navigation APIs.
src/i18n/navigation.ts
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
// Lightweight wrappers around Next.js' navigation
// APIs that take the routing configuration into account
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
5. src/middleware.ts
We can also reuse our routing configuration to set up the middleware. This helps detect the user’s locale and handle redirects accordingly.
src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
// Match all pathnames except for
// - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: "/((?!api|trpc|_next|_vercel|.*\..*).*)",
};
6. src/i18n/request.ts
When using next-intl in Server Components, the relevant configuration is provided by a central module: i18n/request.ts
. This allows the app to load locale-specific messages based on the current request.
src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
// Typically corresponds to the `[locale]` segment
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
7. src/app/[locale]/layout.tsx
The locale matched by the middleware is passed as a parameter. Here, you can validate the locale and use it to configure the HTML document’s language. This is also where we set up the NextIntlClientProvider to pass translations to client components.
src/app/[locale]/layout.tsx
import {NextIntlClientProvider, hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';
export default async function LocaleLayout({
children,
params
}: {
children: React.ReactNode;
params: Promise<{locale: string}>;
}) {
const {locale} = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider>{children}</NextIntlClientProvider>
</body>
</html>
);
}
8. components/language-switcher.tsx
This component allows users to switch between languages.
components/language-switcher.tsx
"use client";
import { useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import { useTransition } from "react";
export function LanguageSwitcher() {
const router = useRouter();
const pathname = usePathname();
const [, startTransition] = useTransition();
const currentLang = pathname.split("/")[1];
const handleLangChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selectedLang = event.target.value;
const segments = pathname.split("/");
segments[1] = selectedLang;
const newPath = segments.join("/");
startTransition(() => {
router.push(newPath);
});
};
return (
<select onChange={handleLangChange} defaultValue={currentLang}>
<option value="id">🇮🇩 Bahasa</option>
<option value="en">🇬🇧 English</option>
</select>
);
}
9. src/app/[locale]/page.tsx
Now you can use translations in your components just like this:
src/app/[locale]/page.tsx
import { LanguageSwitcher } from "@/components/language-switcher";
import { useTranslations } from "next-intl";
export default function HomePage() {
const t = useTranslations("HomePage");
return (
<>
<div className="flex flex-col items-center justify-center h-screen">
<LanguageSwitcher />
<h1 className="text-4xl font-bold mt-10">{t("title")}</h1>
<p className="text-lg">{t("description")}</p>
</div>
</>
);
}
Here is the screenshot of the project with the multilingual feature:
English Language
Bahasa Indonesia Language
And that’s all it takes!
With just a few steps, you've now set up a robust multilingual system using Next.js and next-intl—so your app is ready to serve users in multiple languages, the clean and scalable way.
You can view the complete project on GitHub.