Software localization
A Step-by-Step Guide to Next.js Internationalization
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.
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.
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):
- Next.js (13.0.0)
- i18next (22.0.4)
- react-i18next (12.0.0)
- next-i18next (12.0.1)
- Tailwind CSS (3.1.8)—optional
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:
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:
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@latest
Code 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 i18next
Code 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 thepages
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 ournews
items has a unique slug that we pass in thequery
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!
🔗 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 Platform. 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.
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.
Last updated on October 25, 2023.