How to build multilingual Next.js application

Creating a suitable Next.js sample to try out Gitloc

If you want to create your project from scratch, then you can use the following step-by-step guide, where we will create a multilingual Next.js sample project using next-intl and Gitloc

Create a Next.js Application (more at nextjs.org)

Prerequisites

Make sure you have Node.js and npm installed. You’ll need to have Node.js 18.17 or later on your local development machine. You can use nvm (macOS/Linux), nvm-windows or volta to switch Node versions between different projects.

It's best, if you have some experience with simple HTML, JavaScript and basic Next.js, before jumping to localization. This localization example is not intended to be a Next.js beginner tutorial.

Now let's create a new Next.js project with create-next-app.

To create a new app, you may choose one of the following methods:

npx create-next-app@latest
yarn create next-app
pnpm create next-app
bunx create-next-app

You will then be asked the following prompts:

What is your project named?  my-app
Would you like to use TypeScript?  No / Yes
Would you like to use ESLint?  No / Yes
Would you like to use Tailwind CSS?  No / Yes
Would you like to use `src/` directory?  No / Yes
Would you like to use App Router? (recommended)  No / Yes
Would you like to customize the default import alias (@/*)?  No / Yes

In this sample project we will use App Router. The Next.js App Router introduces support for React Server Components and unlocks many benefits when handling internationalization on the server side.

After the prompts, create-next-app will create a folder with your project name and install the required dependencies.

Once the installation is done, you can open your project folder:

cd my-app

Inside the newly created project, you can run the development server:

  1. Run npm run dev to start the development server.

  2. Visit http://localhost:3000 to view your application.

  3. Edit app/page.tsx (or pages/index.tsx) file and save it to see the updated result in your browser.

Setup next-intl (more at next-intl-docs.vercel.app)

In our example, we will create a project using App Router with 2 languages and separate files for translations.

next-intl integrates with the App Router, by using a [locale] dynamic segment so that we can use this segment to provide content in different languages (e.g. /en, /en/about, etc.).

Let's get started!

Run npm install next-intl and create the following file structure:

├── messages (1)
│   ├── en.json
│   └── ...
├── next.config.js (2)
└── src
    ├── i18n.ts (3)
    ├── middleware.ts (4)
    └── app
        └── [locale]
            ├── layout.tsx (5)
            └── page.tsx (6)

Now, set up the files as follows:

  1. messages/en.json

    Messages can be provided locally or loaded from a remote data source (e.g. a translation management system). Use whatever suits your workflow best.

    The simplest option is to add JSON files in your project based on locales, e.g. en.json.

    messages/en.json
    {
      "Index": {
        "title": "Hello world!"
      }
    }
  2. next.config.js

    Now, set up the plugin which creates an alias to import your i18n configuration (specified in the next step) into Server Components.

    next.config.js
    const withNextIntl = require('next-intl/plugin')();
     
    module.exports = withNextIntl({
      // Other Next.js configuration ...
    });
  3. src/i18n.ts

    next-intl creates a configuration once per request. Here you can provide messages and other options depending on the locale of the user.

    src/i18n.ts
    import {getRequestConfig} from 'next-intl/server';
     
    export default getRequestConfig(async ({locale}) => ({
      messages: (await import(`../messages/${locale}.json`)).default
    }));
  4. middleware.ts

    The middleware matches a locale for the request and handles redirects and rewrites accordingly.

    middleware.ts
    import createMiddleware from 'next-intl/middleware';
     
    export default createMiddleware({
      // A list of all locales that are supported
      locales: ['en', 'de'],
     
      // Used when no locale matches
      defaultLocale: 'en'
    });
     
    export const config = {
      // Match only internationalized pathnames
      matcher: ['/', '/(de|en)/:path*']
    };
  5. app/[locale]/layout.tsx

    The locale that was matched by the middleware is available via the locale param and can be used to configure the document language.

    app/[locale]/layout.tsx
    import {notFound} from 'next/navigation';
     
    // Can be imported from a shared config
    const locales = ['en', 'de'];
     
    export default function LocaleLayout({children, params: {locale}}) {
      // Validate that the incoming `locale` parameter is valid
      if (!locales.includes(locale as any)) notFound();
     
      return (
        <html lang={locale}>
          <body>{children}</body>
        </html>
      );
    }
  6. app/[locale]/page.tsx

    Use translations in your page components or anywhere else!

    app/[locale]/page.tsx
    import {useTranslations} from 'next-intl';
     
    export default function Index() {
      const t = useTranslations('Index');
      return <h1>{t('title')}</h1>;
    }

That's all it takes!

Add translations usage

As you may have already noticed, changing the locale in the URL allows you to translate the page component using the provided t function. You will get the t function by using the useTranslation hook.

Now let's make the app more user-friendly and add some content to translate.

Copy the content from default page app/page.tsx to our localized page app/[locale]/page.tsx and add the t function from the useTranslation hook to it.

app/[locale]/page.tsx
import Image from 'next/image'
import styles from './page.module.css'
import {useTranslations} from 'next-intl';
 
export default function Index() {
  const t = useTranslations('Index');
  return <main className={styles.main}>
    <div className={styles.description}>
      <h1>{t('title')}</h1>
  
      ...
      // rest from app/page.tsx
      ...
  
    </div>
  </main>;
}

Also copy the default page styles from app/page.module.css to app/[locale]/page.module.css or import them from outside.

Define a simple language switcher for our localized layout:

app/[locale]/layout.tsx
import Link from 'next/link';
import { notFound } from 'next/navigation';
import styles from './layout.module.css'

// Can be imported from a shared config
const locales = ['en', 'de'];

function LanguageSwitcher({ locale }) {
  return (
    <div className={styles.switcher}>
      {locales.map((lng) => (
        <Link
          key={lng}
          href={`${lng}/`}
          className={`${styles.link} ${locale === lng && styles.current}`}
        >
          {lng}
        </Link>
      ))}
    </div>
  );
}

export default function LocaleLayout({ children, params: {locale} }) {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();
 
  return (
    <html lang={locale}>
      <body>
        <LanguageSwitcher locale={locale} />
        {children}
      </body>
    </html>
  );
}
app/[locale]/layout.module.css
.switcher {
  position: absolute;
  right: 0;
  padding: 4px;
  z-index: 10;
}

.link {
  margin: 0 4px;
  text-decoration: underline;
}

.current {
  font-weight: bold;
  pointer-events: none;
  text-decoration: none;
}

Messages translation (more at next-intl-docs.vercel.app)

Let's translate all the contents of our page. We have already translated the page title.

app/[locale]/page.tsx
...
    <h1>{t('title')}</h1>
...

Now replace all the text content of the documentation links with the following:

app/[locale]/page.tsx
...
    <a ... >
      <h2>{t('links.docs.title')} <span>-&gt;</span></h2>
      <p>{t('links.docs.message')}</p>
    </a>
    <a ... >
      <h2>{t('links.learn.title')} <span>-&gt;</span></h2>
      <p>{t('links.learn.message')}</p>
    </a>
    <a ... >
      <h2>{t('links.templates.title')} <span>-&gt;</span></h2>
      <p>{t('links.templates.message')}</p>
    </a>
    <a ... >
      <h2>{t('links.deploy.title')} <span>-&gt;</span></h2>
      <p>{t('links.deploy.message')}</p>
    </a>
...

And add this translations to en.json.

messages/en.json
{
  "Index": {
    "title": "Hello world!",
    "links": {
      "docs": {
        "title": "Docs",
        "message": "Find in-depth information about Next.js features and API."
      },
      "learn": {
        "title": "Learn",
        "message": "Learn about Next.js in an interactive course with&nbsp;quizzes!"
      },
      "templates": {
        "title": "Templates",
        "message": "Explore starter templates for Next.js."
      },
      "deploy": {
        "title": "Deploy",
        "message": "Instantly deploy your Next.js site to a shareable URL with Vercel."
      }
    }
  }
}

Next, let's move on to a more complex use of the t-function and add translation for all remaining texts.

Replace the "Get started ..." and "By Vercel" with this:

app/[locale]/page.tsx
...
    <p>
      {t.rich('getStarted', {
        page: (chunks) => <code className={styles.code}>
          src/app/[locale]/page.tsx
        </code>
      })}
    </p>
    <div>
      <a ... >
        {t.rich('links.by', {
          vercel: (chunks) => <Image
            src="/vercel.svg"
            alt="Vercel Logo"
            className={styles.vercelLogo}
            width={100}
            height={24}
            priority
          />
        })}
      </a>
    </div>
...

And add this translations to en.json.

messages/en.json
{
  "Index": {
    "title": "Hello world!",
    "getStarted": "Get started by editing <page>path</page>",
    "links": {
      "by": "By <vercel>Vercel</vercel>",
      ...
    }
  }
}

Now we have all the texts using translations, and we will see that this works if we add de.json and go to /de locale (this step can be done automatically once Gitloc is configured).

messages/de.json
{
  "Index": {
    "title": "Hallo Welt!",
    "getStarted": "Erste Schritte durch Bearbeiten von <page>path</page>",
    "links": {
      "by": "Von <vercel>Vercel</vercel>",
      "docs": {
        "title": "Dokumentation",
        "message": "Finden Sie ausführliche Informationen zu den Funktionen und der API von Next.js."
      },
      "learn": {
        "title": "Lernen",
        "message": "Erfahren Sie mehr über Next.js in einem interaktiven Kurs mit Quizfragen!"
      },
      "templates": {
        "title": "Vorlagen",
        "message": "Entdecken Sie Starter-Vorlagen für Next.js."
      },
      "deploy": {
        "title": "Bereitstellen",
        "message": "Stellen Sie Ihre Next.js-Site sofort auf einer gemeinsam nutzbaren URL mit Vercel bereit."
      }
    }
  }
}

Finally, сonnect your local project folder to your new repository on GitHub.

Done! Now you can move to the next step - connect remote repository to Gitloc

Last updated