Software localization

A Step-by-Step Guide to Next.js Internationalization

Learn how to build a news reader app in Next.js and level up your Next.js i18n skills to successfully launch in international markets.
React l10n and i18n blog post featured image | Phrase

Next.js is one of the most popular frameworks for React. To help you implement multilingual support for your app, this tutorial will guide you through all of the Next.js internationalization (i18n) essentials.

We will localize our UI component content using the next-i18next library and localize routes using Next’s native i18n features. Finally, we will make sure to cover i18n for both SSR (server-side rendering) and SSG (static site generation). Let’s begin!

🔗 Resource » For localizing a Next.js app using the App Router and next-intl. library, please feel free to take a look at our tutorial on Next.js App Router localization with next-intl.

String Management UI visual | Phrase

Phrase Strings

Take your web or mobile app global without any hassle

Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.

Explore Phrase Strings

Native Next.js i18n

Since v10, Next.js has had built-in support for internationalized routing. With this addition, given some locale configuration, Next.js automatically handles localized routing.

Included out of the box in Next.js are sub-path and domain routing. Sub-path routing is where the locale is part of the URL, like /blog, /fr/blog, /es/blog. Domain routing has the locale appear in the top-level domain, like mywebsite.com/blog, mywebsite.fr/blog, mywebsite.nl/blog.

Of course, we will need to configure the i18n in our app for these routes to work. Let’s see this in practice by building a small demo app.

Libraries we will be using

We will be using the following NPM packages (with the version number in parentheses):

Our app demo: i18n News Reader

We will be building a news reader app in Next.js. Here’s what its homepage will look like when we’re done:

i18n News Reader Homepage | Phrase

The homepage will list news articles in a card format, presenting them in the default browser locale.

🗒️ Note » Next.js automatically detects the default locale preferred by the user based on the Accept-Language header.

If we want to read the same webpage in a different language, say Spanish, we will be able to choose it from a language selector drop-down beside the app title. Here’s what the home page will look like when Spanish ( es) is selected:

i18n News Reader Homepage in Spanish | Phrase

To read a news article in detail, we will click on its Read more button. Next.js will route to the article in the currently selected locale, and we will see the news detail page in our preferred language. That’s our app in a nutshell. Let’s build it!

Initial setup

Let’s start from scratch and spin up a new Next.js app with the following command-line command:

$ npx create-next-app@latestCode language: Bash (bash)

Follow the prompts and give your project a suitable name.

Let’s build our homepage, starting with adding a Header component, which has the app title along with a LanguageSwitcher component (we will make that one next):

// components/Header.jsx

import Link from 'next/link';
import LanguageSwitcher from './LanguageSwitcher';

export default function Header() {
  return (
    {/* CSS classes removed for brevity. */}
    <header>
      <div>
        <Link href='/'>
          <h2>
            i18n News Reader
          </h2>
        </Link>

        <LanguageSwitcher />
      </div>
    </header>
  );
}Code language: JavaScript (javascript)

🔗 Resource » You can get the complete code of our demo app, including CSS styles, from our GitHub repo.

For the LanguageSwitcher component, we have a select element with two option values:

// components/LanguageSwitcher.jsx

export default function LanguageSwitcher() {
  return (
    <div>
      <select>
        <option value='en'>English</option>
        <option value='es'>Español</option>
      </select>
    </div>
  );
}Code language: JavaScript (javascript)

Let’s include our Header in the app.js file:

// pages/app.js

import Header from '../components/Header';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Header />
      <main>
        <Component {...pageProps} />
      </main>
    </>
  );
}

export default MyApp;Code language: JavaScript (javascript)

Displaying localized news from a backend

Now we need to work on news items. The news data will be served from a separate Express.js app that simulates a production backend API. The mock backend code is a bit outside the scope of this article; all we need to know for our purposes is how to fetch data from it. Two endpoints, http://localhost:3000/en and http://localhost:3000/es, serve news data from English and Spanish JSON files, respectively.

// data/en.json

[
  {
    "id": 1,
    "slug": "energy-storage-system-market-demand-is-projected-to-reach-44071-gw-in-2028",
    "title": "Energy Storage System Market demand is Projected to Reach 440.71 GW in 2028",
    "description": "...",
    "content": "...",
    "date": "August 16, 2022"
  },
 
  // ...
]Code language: JSON / JSON with Comments (json)
// data/es.json

[
  {
    "id": 1,
    "slug": "energy-storage-system-market-demand-is-projected-to-reach-44071-gw-in-2028",
    "title": "Se prevé que la demanda del mercado de sistemas de almacenamiento de energía alcance los 440,71 GW en 2028",
    "description": "...",
    "content": "...",
    "date": "16 de agosto de 2022"
  },
  
  // ...
]Code language: JSON / JSON with Comments (json)

🔗 Resource » If you’re curious, you can get the Express backend code from our GitHub repo.

Let’s fetch this news data. For this, we will export Next’s getServerSideProps for server-side rendering (SSR), which pre-renders our index.js page on each request:

// pages/index.js

export const getServerSideProps = async (context) => {
  const { locale } = context;
  const res = await fetch(`http://localhost:3001/${locale}`);
  const data = await res.json();

  return {
    props: {
      data,
    },
  };
};Code language: JavaScript (javascript)

Note that we are using the context parameter, made available to us in getServerSideProps, to get the active locale. This locale’s value will be what Next.js automatically detects from the browser’s Accept-Language header (with a fallback, see following note). In our case it will be either en (English) or es (Spanish).

🗒️ Note » If the automatically detected Accept-Language locale is not supported by our app, Next will use our configured default locale instead. We cover this configuration in the next section.

In the same pages/index.js file, we will use the data property returned from getServerSideProps to render our news items in the Home component.

// pages/index.js

import Head from 'next/head';

// `data` is returned from getServerSideProps and is 
// available as a component prop here.
export default function Home({ data }) {
  return (
    <div>
      <Head>
        <title>i18n News Reader</title>
      </Head>

      <div>
        {data.map((news, index) => (
          <div key={index}>
            <p>{news.date}</p>
            
            <h3>{news.title}</h3>
            
            <p>{news.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export const getServerSideProps = async (context) => {
  // ...
};Code language: JavaScript (javascript)

Using next-i18next to localize component strings

Let’s start localizing our UI components using the next-i18next library. next-i18next provides a way to manage translated content and is built on top oBy default, next-i18next expects the translations to be at /public/locales/[localeName]/common.json. So let’s add our English translations to public/locales/en/common.json.f the very popular i18next library.

First, let’s install next-i18next and its peer dependencies by running the following from the command-line:

$ npm i next-i18next react-i18next i18nextCode language: Bash (bash)

By default, next-i18next expects the translations to be at /public/locales/[localeName]/common.json. So let’s add our English translations to public/locales/en/common.json.

// public/locales/en/common.json 

{
  "app_title": "i18n News Reader",
  "active_locale": "English",
  "button_label": "Read More",
  "homepage_nav_link_label": "Go Back"
}Code language: JSON / JSON with Comments (json)

Our Spanish translations will, of course, go in their respective JSON file.

// public/locales/es/common.json 

{
  "app_title": "Lector de noticias i18n",
  "active_locale": "Español",
  "button_label": "Lee mas",
  "homepage_nav_link_label": "Regresa"
}Code language: JSON / JSON with Comments (json)

It’s time to set up our i18n configuration files, starting with next-i18next.config.js at the root of our project.

// next-i18next.config.js

module.exports = {
  debug: process.env.NODE_ENV === 'development',
  i18n: {
    locales: ['en', 'es'],
    defaultLocale: 'en',
  },
};Code language: JavaScript (javascript)

🔗 Resource » We can customize the file paths to our translation files. Find out how in the official documentation.

🗒️ Note » As we mentioned earlier, Next will fall back to the defaultLocale if the automatically detected locale is not in our supported locale list. We can disable automatic locale detection. We can also override it using a special cookie.

This syntax will be familiar to you if you’ve worked with react-i18next. We’re simply defining what our supported and default locales are.

To keep our config consistent between next-i18next and Next’s own internationalized routing, we can pass our i18n config to Next’s next.config.js file:

// next.config.js

const { i18n } = require('./next-i18next.config.js');

module.exports = {
 //...
 i18n, 
};Code language: JavaScript (javascript)

Now let’s wrap our MyApp with the appWithTranslation Higher-Order Component (HOC), which is responsible for adding an I18nextProvider to our app.

// pages/_app.js

import { appWithTranslation } from 'next-i18next';
import Header from '../components/Header';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Header />
      <main>
        <Component {...pageProps} />
      </main>
    </>
  );
}

export default appWithTranslation(MyApp);Code language: JavaScript (javascript)

When we call appWithTranslation, next-i18next will wrap our app with an I18nextProvider. This ensures that an instance of an i18next object, aware of our active locale and its translations, is available to all our components. We can then call a handy useTranslation hook to fetch our translations.

Let’s do just that by translating our Header component. To render translations in JSX, we can destructure a t() function from useTranslation.

// components/Header.jsx

import { useTranslation } from 'next-i18next';
import Link from 'next/link';

export default function Header() {
  const { t } = useTranslation();

  return (
    <header>
      <div>
        <Link href='/'>
          <h2>
            {/* We simply give t() our translation key, and
                it renders its value in the active locale. */}
            {t('app_title')}
          </h2>
        </Link>
        <LanguageSwitcher />
      </div>
    </header>
  );
}Code language: JavaScript (javascript)

Next, let’s translate the homepage. Our Home component is rendered on the server. So, to translate it, we will need to use the serverSideTranslations function provided by next-i18next. During SSR, this function helps pass translations and config as props to our page components. Here’s how to use it with getServerSideProps:

// pages/index.js

import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

import Head from 'next/head';
import Link from 'next/link';

export default function Home({ data }) {
  const { t } = useTranslation();

  return (
    <div>
      <Head>
				{/* Just as before, we use t('message_key') to 
            translate. */}
        <title>{t('app_title')}</title>
      </Head>
      <div>
        {/* ... */}
      </div>
    </div>
  );
}

export const getServerSideProps = async (context) => {
  const { locale } = context;
  const res = await fetch(`http://localhost:3001/${locale}`);
  const data = await res.json();

  return {
    props: {

      // Spread the returned object into our `props` to expose
      // them to our component during SSR.
      ...(await serverSideTranslations(locale, ['common'])),

      data,
    },
  };
};Code language: JavaScript (javascript)

We are passing the extracted locale from the context along with the translation namespace in the array. Note that we are adding the common namespace explicitly here. We can pull one or more namespaces as long as they match our namespace file paths (e.g. common.json). common is the default namespace, hence it is optional to use it here.

🤿 Go deeper » Read more about namespaces in the i18next documentation.

Until now, we’ve only worked with the homepage. Let’s generate localized dynamic pages for our news articles and get the language selection drop-down working.

Working with localized routes

In order to create localized routes, first we need to make the locale in getServerSideProps available to our Home component:

// pages/index.js

// `locale` is returned from `getServerSideProps` 
// below, and is available as a component prop here.
export default function Home({ data, locale }) {

  // We will look at using `locale` here in a moment

}


export const getServerSideProps = async (context) => {
  const { locale } = context;
  const res = await fetch(`http://localhost:3001/${locale}`);
  const data = await res.json();

  return {
    props: {
      data,
      
      // We expose the `locale` to the `Home` 
      // component above.
			locale 
    },
  };
};Code language: JavaScript (javascript)

Now we can use the <Link> component provided by Next.js to create dynamic, localized links. We do this by passing the locale prop to our <Link>s.

// pages/index.js

//...
import Link from 'next/link';

export default function Home({ data, locale }) {
  const { t } = useTranslation();

  return (
    <div>
      <Head>
        <title>{t('app_title')}</title>
      </Head>
      <div>
        {data.map((news, id) => (
          <div key={id}>
            <p>{news.date}</p>

            {/* The article title now navigates to
                the article details page... */}
            <Link
              href={{
                pathname: '/news/[slug]',
                query: { slug: news.slug },
              }}
              locale={locale}
            >
              <h3>{news.title}</h3>
            </Link>

            <p>{news.description}</p>

            {/* ...the "Read more" button also goes to the
                article details page. */}
            <Link
              href={{
                pathname: '/news/[slug]',
                query: { slug: news.slug },
              }}
              locale={locale}>
              {t('button_label')}
            </Link>

          </div>
        ))}
      </div>
    </div>
  );
}

export const getServerSideProps = async (context) => {
  //...
};Code language: JavaScript (javascript)

Look at the href prop of the <Link> component. We pass a URL object to it with the pathname and query properties. Let’s see what both of these are:

  • pathname is the page’s path in the pages directory. In our case, we want to navigate to the dynamic /pages/news/[slug].js page, so our path is /news/[slug].
  • query is an object with the dynamic segment, slug in this case. Each of our news items has a unique slug that we pass in the query object.

When our <Link> renders, we get an <a> tag that looks like the following.

<a href="/news/energy-storage-system-market-demand-is-projected-to-reach-44071-gw-in-2028"></a>Code language: HTML, XML (xml)

By default, if we pass our default locale to <Link> as the locale prop, we will get a URL without a locale component. So, in our case, if we pass en as the locale, we will get a URL like the one above.

However, when we change the translation language to Spanish (es) via the LanguageSwitcher component, our <Link>s will add the locale as the first part of the URL:

<a href="/es/news/energy-storage-system-market-demand-is-projected-to-reach-44071-gw-in-2028"></a>Code language: HTML, XML (xml)

Statically generating localized pages

Next’s built-in SSG (Static Site Generation) allows us to pre-render pages that don’t have dynamic content or that change infrequently, speeding up initial page loads. Localizing our statically-generated pages isn’t too difficult because Next.js provides our configured locales to its getStaticPaths SSG function. Let’s use this to create localized news details pages in our demo.

// pages/news/[slug].js

// ...

// We destructure our `locales` array from the 
// context parameter object.
export const getStaticPaths = async ({ locales }) => {

  // Grab the news data from our mock backend for
  // our default locale.
  const res = await fetch('http://localhost:3001/');
  const data = await res.json();

  // Create an array of path objects, one for each 
  // locale + news item. For example, if we had 
  // two news item with slugs 'foo' and 'bar', our 
  // `paths` array would contain four entries:
  //   - { params: { slug: 'foo' }, locale: 'en' }
  //   - { params: { slug: 'foo' }, locale: 'es' }
  //   - { params: { slug: 'bar' }, locale: 'en' }
  //   - { params: { slug: 'bar' }, locale: 'es' }
  const paths = data.flatMap((news) => {
    return locales.map((locale) => ({
      params: {
        slug: news.slug,
      },
      locale,
    }));
  });

  return {
    paths,

    // All our news pages are pre-built and any path *not*
    // declared above results in a 404 (Not Found) error 
    // page.
    fallback: false,
  };
};Code language: JavaScript (javascript)

Note that to generate paths here, we need to tell Next.js what locales to build the paths from. You’ll remember that we defined our locales in next-i18next.config.js and passed them on to Next’s next.config.js. Next.js will, in turn, make these locales available to getStaticPaths. So, we will create a news article page for each of our configured supported locales, English (en) and Spanish (es).

Now we also need to have the getStaticProps function for (SSG), and it will look like this:

// pages/news/[slug].js

// ...

export const getStaticProps = async (context) => {
  const { slug } = context.params;
  const { locale } = context;

  const res = await fetch(`http://localhost:3001/${locale}/${slug}`);
  const data = await res.json();

  return {
    props: {
      ...(await serverSideTranslations(locale, ['common'])),
      data,
    },
  };
};Code language: JavaScript (javascript)

Here we get both the slug and locale from the params we added in the getStaticPaths function. We are using these values to fetch the news item’s localized details from our mock backend. For example, hitting http://localhost:3001/es/deliveroo-confirms-dutch-exit-next-month would give us JSON that looked like the following.

{
  "id": 4,
  "slug": "deliveroo-confirms-dutch-exit-next-month",
  "title": "Deliveroo confirma su salida holandesa el prĂłximo mes",
  "description": "...",
  "date": "05 de marzo de 2022"
}Code language: JSON / JSON with Comments (json)

Also, note that we are using next-i18next‘s serverSideTranslations method the same way we did on our homepage, ensuring our translated UI strings are also available to our component during SSG.

With that, we should be able to use the returned data from getStaticProps and render it in our JSX.

// pages/news/[slug].js

import { useTranslation } from 'next-i18next';
import Link from 'next/link';

export default function NewsDetail({ data }) {
  const { t } = useTranslation();

  return (
    <div>
      <div>
        <Link href='/'>{t('homepage_nav_link_label')}</Link>
        <p>{data.date}</p>
      </div>

      <article>
        <h1>{data.title}</h1>
        <div>
          <p>{data.content}</p>
        </div>
      </article>
    </div>
  );
}

export const getStaticProps = async (context) => {
  // ...
}

export const getStaticPaths = async ({ locales }) => {
  // ...
}Code language: JavaScript (javascript)

Making our language switcher functional

To make our LanguageSwitcher component work as expected, we will need to use the useRouter hook from the next/router package.

// components/LanguageSwitcher.jsx

import { useRouter } from 'next/router';

export default function LanguageSwitcher() {
  const router = useRouter();

  return (
    <div>
      <select onChange={(e) =>
          router.push(
            {
              pathname: router.pathname,
              query: router.query,
            },
            null,
            { locale: e.target.value }
          )
        }
      >
        <option value='en'>English</option>
        <option value='es'>Español</option>
      </select>
    </div>
  );
}Code language: JavaScript (javascript)

The router object returned by useRouter allows us to programmatically navigate to our localized routes with router.push(). On any given page, we want only want to change the locale part of the route when switching languages. For example, if we’re on /news/foo (default English route), switching to Spanish should take us to /es/news/foo. To accomplish this, we pass the router’s pathname and query back to the router unchanged, only updating the locale to match the one the user selected from the drop-down.

🗒️ Note » The second argument to router.push() is an optional decorator called as. This can be used to set the value shown in the browser URL bar. (More info on that in the router.push() documentation.) We’re not really using as here, so we pass in null for its value.

With all that in place, your LanguageSwitcher component should work as expected. You can now read the news list on the homepage and click on each to get the news detail page in the default language. To read it in another language, you can change it from the drop-down and enjoy!

News detail page translation | Phrase

🔗 Resource » You can get the complete code of our demo app from our GitHub repo.

Ready to start translating

We truly hope you enjoyed building a news reader app in Next.js with multilingual support. We used the next-i18next library for component-level string i18n configurations and tweaked our SSG code to generate localized pages, ensuring our pages load smoothly.

If you’re ready to level up your app translation process, check out the Phrase Localization Suite. With its dedicated software localization solution, Phrase Strings, you can manage translation strings for your web or mobile app more easily and efficiently than ever.

With a robust API to automate translation workflows and integrations with GitHub, GitLab, Bitbucket, and other popular software platforms, Phrase Strings can do the heavy lifting for you to stay focused on your code.

Phrase Strings provides a comprehensive strings editor that enables translators to access the content you have pushed for translation. Once the translation is complete, you can effortlessly retrieve the translated content and incorporate it back into your project.

As your software product grows, our integrated suite will enable you to seamlessly connect Phrase Strings with a cutting-edge translation management system (TMS), so you can unlock the full power of traditional CAT tools and machine translation capabilities.

Check out all Phrase features for developers and see for yourself how they can streamline your software localization workflows from day one.

String Management UI visual | Phrase

Phrase Strings

Take your web or mobile app global without any hassle

Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.

Explore Phrase Strings