Next.js 15 App Router Localization with next-intl

Next.js 15 App Router Localization with next-intl

Timo
Timo

In this article, I will illustrate how to implement next-intl localization in a Next.js 15 application.

Localization is a crucial feature for any application aiming to cater to a global audience, ensuring that users can interact with content in their preferred language.

Overview

  1. Packages
  2. How to install and configure next-intl?
    Step 1: Create a fresh Next.js project.
    Step 2: Install the next-intl package.
    Step 3: Adding the locale route segment.
    Step 4: Adding the translation message files.
    Step 5: Setting up configuration files.
    Step 6: Create a localization middleware.
    Step 7: How to retrieve translated contents.
  3. Demo
  4. Conclusion

Packages

Lib/technology Version Description
Typescript 5.4.5 Our programming language
Next.js 15.0.0 The fullstack react framework
React 18.3.1 For building user interface
next-intl 3.15.3 i18n lib for Next.js app router
rtl-detect 1.1.2 Detects right to left languages
Tailwind CSS 3.4.3 Utility-first CSS framework for styling
@mui/material 5.15.20 Material design UI component lib

How to install and configure next-intl?

Step 1: Create a fresh Next.js project.

npx create-next-app i18n-next-app cd i18n-next-app yarn add next@latest

Step 2: Install the next-intl package:

yarn add next-intl

Step 3: Adding the locale route segment

*** First, we need to follow the documents about locale codes and dynamic route segments.

Next, the subdirectory called [locale] is created under src/app.

***Note, the module import should be updated if needed.

Step 4: Adding the translation message files

First, create a new directory called locales under src

In this tutorial, we are supporting en-us (English USA) and vi (Vietnamese).

Continue, add the content for translation files:

src/locales/en-us/about.ts
export default {
  title: 'Title',
  description: 'Description',
} as const;
src/locales/vi/about.ts
export default {
  title: 'Tiêu đề',
  description: 'Miêu tả',
} as const;
src/locales/vi/index.ts src/locales/vi/index.ts
import about from './about';
export default { about };

Step 5: Setting up configuration files

next.config.mjs
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {};

export default withNextIntl(nextConfig);

Create a new file called

i18n.config.ts
import { createSharedPathnamesNavigation } from 'next-intl/navigation';

export const locales = ['en-us', 'vi'] as const;
export type Locale = (typeof locales)[number];

export const localeNames: Record<Locale, string> = {
  'en-us': 'Tiếng anh',
  vi: 'Vietnamese',
};

export const { Link, usePathname, useRouter } = createSharedPathnamesNavigation({ locales });

The next-intl uses the i18n.ts file to load translations.

src/global.d.ts
type Messages = (typeof import('./locales/en-us'))['default'];

declare interface IntlMessages extends Messages {}
src/i18n.ts
import { getRequestConfig } from 'next-intl/server';
import { notFound } from 'next/navigation';

import { type Locale, locales } from ' ./i18n.config';

export default getRequestConfig(async ({ locale }) => {
  if (!locales.includes(locale as Locale)) {
    return notFound();
  }

  const messages = await import(`./locales/${locale}/index.ts`);

  return {
    messages: messages.default,
  };
});

Step 6: Create a localization middleware

src/middlewares/with-localization.ts
import createMiddleware from 'next-intl/middleware';
import { NextMiddleware } from 'next/server';

import { locales } from '../i18n.config';
import { MiddlewareFactory } from './types';

export const withLocalization: MiddlewareFactory = (next: NextMiddleware) =>
  createMiddleware({
    defaultLocale: 'vi',
    locales,
    localeDetection: false,
  });
src/middleware.ts
import { stackMiddlewares } from './middlewares/stack-middlewares';
import { withLocalization } from './middlewares/with-localization';

export default stackMiddlewares([
  withLocalization,
]);

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Step 7: How to retrieve translated contents

src/hooks/useTextDirection.ts
import { useLocale } from 'next-intl';
import { isRtlLang } from 'rtl-detect';

export type TextDirection = 'ltr' | 'rtl';

export default function useTextDirection(): TextDirection {
  const locale = useLocale();

  return isRtlLang(locale) ? 'rtl' : 'ltr';
}
src/[locale]/layout.tsx
import { Metadata, Viewport } from 'next';
import { NextIntlClientProvider, useMessages } from 'next-intl';
import { unstable_setRequestLocale } from 'next-intl/server';

import { locales } from './src/i18n.config';

import useTextDirection from '@/hooks/useTextDirection';
import './globals.css';

export async function generateMetadata(): Promise<Metadata> {
  return {
    title: {
      default: site.header,
      template: `${site.header} | %s`,
    },
  };
}

export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

export default function RootLayout({
  children,
  params: { locale },
}: Readonly<{
  children: React.ReactNode;
  params: { locale: string };
}>) {
  unstable_setRequestLocale(locale);
  const dir = useTextDirection();
  const messages = useMessages();

  return (
    <html lang={locale} dir={dir} className={combineClasses([roboto.className])}>
      <head></head>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
src/app/[locale]/about.page.tsx
import { getTranslations } from 'next-intl/server';

export async function generateMetadata({
  params: { locale },
}: {
  params: { locale: string };
}): Promise<Metadata> {
  const t = await getTranslations({
    locale,
  });

  return {
    title: t('about.title'),
  };
}

export default async function About({ params: { locale } }: { params: { locale: string } }) {
  const t = useTranslations('about');

  return (
    <Layout>
      {t('description')}
    </Layout>
  );
}

Demo

Vietnamese: https://www.dinhthanhcong.info/vi/about

English: https://dinhthanhcong.info/en-us/about

Conclusion

By following these guidelines, you can build a Next.js 15 application that meets the technical requirements of localization and delivers a superior user experience for a diverse audience.

Good luck to you, I hope this post is of value to you!!!!

Reference Documents:

  1. https://saimana.com/list-of-country-locale-code/
  2. https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
  3. https://next-intl-docs.vercel.app/docs/getting-started/app-router/without-i18n-routing
  4. https://www.dinhthanhcong.info/vi/articles/43