Internationalization with I18n in NextJs projects

October 5, 2024 (2mo ago)

Internationalization with I18n in NextJs projects

In this article, I will try to be brief and quickly and clearly teach how to add internationalization to a NextJs project, following the example used on my own website.

For this tutorial, we will use the app router without using i18n routing (that is, the translation will not be tied to the route, example: "example.com/en").

Additionally, we will use a cookie so that the website can be delivered in the correct language to the user. This cookie will be set on the user's first load of the website and can be reset later by a selector component.

Project Setup

If you haven't already, start your Next.Js project with the App Router configuration.

Next, install the next-intl package:

npm install next-intl

Then, we will create the following file structure:


├── i18n
	├── dictionaries
		├── en.json (1)
		└── ...
	├── locales.ts (2)
	├── request.ts (3)
	└── setLocale.ts (4)
├── next.config.mjs (5)
└── app
	├── layout.tsx (6)
    └── page.tsx (7)

i18n/dictionaries/en.json

The i18n folder will store everything related to localization and internationalization to facilitate project structuring.

The dictionaries folder will store our translations. These files can be loaded statically or dynamically. For simplicity, we will be adding them as files.

i18n/dictionaries/en.json

{
  "HomePage": {
    "title": "Hello world!"
  }
}

i18n/locales.ts

To store our constants about localization. (This can also be stored dynamically or even become an environment variable)

i18n/locales.ts

export const SUPPORTED_LOCALES = ['en', 'pt']; // Available translations
export const DEFAULT_LOCALE = 'en'; // Default language used as fallback
export const LOCALE_COOKIE_NAME = 'NEXT_LOCALE'; // Cookie key that we will use to get the translation from the server

i18n/request.ts

Next-intl will use the request to define how to proceed with server components that use internationalization. The request file handles the logic to determine the language. Our approach here will be to use cookies to observe the language to be used.

i18n/request.ts

import { getRequestConfig } from 'next-intl/server';
import { cookies } from 'next/headers';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from './locales';

export default getRequestConfig(async () => {
  const cookieLocale = cookies().get('NEXT_LOCALE')?.value;

  // Determine the locale
  const locale = cookieLocale || DEFAULT_LOCALE;

  // Check if it is supported or return the default
  const finalLocale = SUPPORTED_LOCALES.includes(locale)
    ? locale
    : DEFAULT_LOCALE;

  return {
    locale: finalLocale,
    messages: (await import(`./dictionaries/${finalLocale}.json`)).default,
  };
});

i18n/setLocale.ts

This file will serve as a utility so that we can set cookies on the server side.

i18n/setLocale.ts

'use server';

import { cookies } from 'next/headers';
import { LOCALE_COOKIE_NAME } from './locales';

export async function setUserLocale(locale: string) {
  cookies().set(LOCALE_COOKIE_NAME, locale);
}

next.config.mjs

Now, configure the plugin that creates an alias to provide a request-specific i18n configuration to server components (which we created as i18n/request.ts).

next.config.mjs

import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();

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

export default withNextIntl(nextConfig);

app/layout.tsx

The language code provided in i18n/request.ts is available via getLocale and can be used to set the document's language. Additionally, we can use this location to pass the i18n/request.ts configuration to client components via NextIntlClientProvider.

app/layout.tsx

import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const locale = await getLocale();

  // Providing all messages to the client
  // side is the easiest way to get started
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

app/page.tsx

To use your translations, simply use the useTranslations hook in your components and pages!

app/page.tsx

import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

Changing the language on the first load

Although your project will already work with internationalization from the setup, to be able to change the language, we need to create a client-side component that can read the browser's language and change the cookie. To do this, just create a component as follows:

import { useEffect } from 'react';
import {
  SUPPORTED_LOCALES,
  DEFAULT_LOCALE,
  LOCALE_COOKIE_NAME,
} from '../../i18n/locales';
import { setUserLocale } from '../../i18n/setLocale';

export default function LocaleSetter() {
  useEffect(() => {
    // Check if the locale cookie already exists
    if (document.cookie.includes(LOCALE_COOKIE_NAME)) return;

    // Remove the country from the locale (e.g., pt-BR and pt-PT becomes pt)
    const navigatorLang = navigator.language.split('-')[0];
    let locale = SUPPORTED_LOCALES.includes(navigatorLang)
      ? navigatorLang
      : DEFAULT_LOCALE;

    // Change cookie
    setUserLocale(locale);
  }, []);

  return null;
}

Thus, we can include LocaleSetter in our app/layout.tsx and our application will already have translations set by the browser's language!

From here, you can also create a selector component and use the setUserLocale function to have the language redefined!


See you next time ;D

And don't forget to check out other posts on my blog