Skip to main content

Next.js Internationalization

Introduction

Internationalization (often abbreviated as i18n - "i" + 18 characters + "n") is the process of designing and preparing your application to be usable in different languages and regions. As your application grows globally, supporting multiple languages becomes crucial for reaching a wider audience.

Next.js provides built-in internationalization support, making it relatively straightforward to create multi-language websites. In this guide, we'll explore how to implement internationalization in your Next.js applications, covering both routing and content translation.

Why Internationalization Matters

Before diving into the implementation details, let's understand why internationalization is important:

  • Broader audience reach: Supporting multiple languages helps you connect with users worldwide.
  • Better user experience: Users prefer content in their native language.
  • SEO benefits: Localized content can improve search engine rankings in specific regions.
  • Compliance requirements: Some regions require content to be available in local languages.

Next.js Internationalization Fundamentals

Next.js offers two internationalization strategies:

  1. Sub-path routing: /en/about, /fr/about
  2. Domain routing: example.com/about, example.fr/about

We'll focus primarily on sub-path routing as it's more common for most applications.

Setting Up Internationalization in Next.js

Step 1: Configure i18n in next.config.js

First, we need to configure the internationalization settings in our next.config.js file:

js
/** @type {import('next').NextConfig} */
const nextConfig = {
i18n: {
// List of locales supported by your application
locales: ['en', 'fr', 'es', 'de'],

// Default locale
defaultLocale: 'en',

// Optional: domain specific locales
// domains: [
// {
// domain: 'example.com',
// defaultLocale: 'en',
// },
// {
// domain: 'example.fr',
// defaultLocale: 'fr',
// },
// ],
},
}

module.exports = nextConfig

This configuration tells Next.js:

  • Which languages we support (en, fr, es, de)
  • What our default language is (en)
  • Optionally, we could specify domain-specific locales (commented out above)

Step 2: Create Translation Files

Let's create some JSON files to store our translations. Create a /locales folder in the root of your project with subfolders for each language:

/locales
/en
common.json
/fr
common.json
/es
common.json
/de
common.json

Now let's add some translations to each file:

English (/locales/en/common.json):

json
{
"greeting": "Hello, world!",
"description": "Welcome to our Next.js application",
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
}
}

French (/locales/fr/common.json):

json
{
"greeting": "Bonjour, le monde!",
"description": "Bienvenue sur notre application Next.js",
"nav": {
"home": "Accueil",
"about": "À propos",
"contact": "Contact"
}
}

Step 3: Create a Translation Hook

Next, let's create a hook to easily access our translations. We'll use the next-i18next library for this.

First, install the required packages:

bash
npm install next-i18next react-i18next i18next

Now, create a next-i18next.config.js file in your project root:

js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'es', 'de'],
},
localePath: './locales',
}

Then, create a useTranslation.js hook:

js
import { useRouter } from 'next/router'
import { useTranslation as useTranslationOriginal } from 'next-i18next'

export function useTranslation() {
const router = useRouter()
const { locale } = router

const { t } = useTranslationOriginal('common')

return { t, locale }
}

Step 4: Update the _app.js File

Next, we need to update our _app.js file to include the translation provider:

jsx
import { appWithTranslation } from 'next-i18next'
import nextI18NextConfig from '../next-i18next.config.js'

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

export default appWithTranslation(MyApp, nextI18NextConfig)

Step 5: Implement on Pages

Now we can use our translations in our pages:

jsx
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from '../hooks/useTranslation'
import Link from 'next/link'
import { useRouter } from 'next/router'

export default function Home() {
const { t, locale } = useTranslation()
const router = useRouter()

const changeLanguage = (e) => {
const locale = e.target.value
router.push(router.pathname, router.asPath, { locale })
}

return (
<div className="container">
<header>
<select onChange={changeLanguage} defaultValue={locale}>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="de">Deutsch</option>
</select>
<nav>
<Link href="/" locale={locale}>
{t('nav.home')}
</Link>
<Link href="/about" locale={locale}>
{t('nav.about')}
</Link>
<Link href="/contact" locale={locale}>
{t('nav.contact')}
</Link>
</nav>
</header>

<main>
<h1>{t('greeting')}</h1>
<p>{t('description')}</p>
</main>
</div>
)
}

export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
},
}
}

Language Switching Implementation

Let's create a more reusable language switcher component:

jsx
import { useRouter } from 'next/router'

export default function LanguageSwitcher() {
const router = useRouter()
const { locales, locale: activeLocale } = router

const handleChange = (e) => {
const locale = e.target.value
router.push(router.pathname, router.asPath, { locale })
}

return (
<select onChange={handleChange} defaultValue={activeLocale}>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale === 'en' && 'English'}
{locale === 'fr' && 'Français'}
{locale === 'es' && 'Español'}
{locale === 'de' && 'Deutsch'}
</option>
))}
</select>
)
}

Handling Date and Number Formatting

Beyond simple text translations, you'll often need to format dates and numbers according to different locales. The Intl API is perfect for this:

jsx
function FormattedDate({ date, locale }) {
const formattedDate = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)

return <span>{formattedDate}</span>
}

function FormattedNumber({ number, locale }) {
const formattedNumber = new Intl.NumberFormat(locale, {
style: 'currency',
currency: locale === 'en' ? 'USD' : locale === 'fr' ? 'EUR' : 'USD'
}).format(number)

return <span>{formattedNumber}</span>
}

// Usage example
export default function ProductPage() {
const { locale } = useRouter()
const date = new Date()
const price = 29.99

return (
<div>
<p>Release Date: <FormattedDate date={date} locale={locale} /></p>
<p>Price: <FormattedNumber number={price} locale={locale} /></p>
</div>
)
}

Right-to-Left (RTL) Language Support

For languages like Arabic, Hebrew, and Persian that are written from right to left, you'll need additional support:

jsx
import { useRouter } from 'next/router'

export default function Layout({ children }) {
const { locale } = useRouter()
const rtlLanguages = ['ar', 'he']
const isRtl = rtlLanguages.includes(locale)

return (
<div dir={isRtl ? 'rtl' : 'ltr'}>
{children}
</div>
)
}

You might also want to use CSS logical properties instead of directional ones:

css
.container {
margin-inline-start: 20px; /* Instead of margin-left or margin-right */
padding-inline-end: 15px; /* Instead of padding-right or padding-left */
}

SEO Considerations

For better SEO with internationalized websites, add the hreflang tags to help search engines understand your language versions:

jsx
import Head from 'next/head'
import { useRouter } from 'next/router'

export default function SEOHead() {
const router = useRouter()
const { locales, defaultLocale, locale: currentLocale } = router
const canonicalPath = router.asPath.split('?')[0]

return (
<Head>
<link
rel="canonical"
href={`https://yourwebsite.com${canonicalPath}`}
/>
{locales.map((locale) => (
<link
key={locale}
rel="alternate"
hrefLang={locale}
href={`https://yourwebsite.com${
locale === defaultLocale ? '' : `/${locale}`
}${canonicalPath}`}
/>
))}
</Head>
)
}

Advanced: Dynamic Content from CMS

For real-world applications, translations are often stored in a CMS rather than static JSON files. Here's an example using a hypothetical API:

jsx
import { useState, useEffect } from 'react'
import { useRouter } from 'next/router'

export default function BlogPost({ slug }) {
const { locale } = useRouter()
const [article, setArticle] = useState(null)
const [loading, setLoading] = useState(true)

useEffect(() => {
async function fetchArticle() {
setLoading(true)
try {
const response = await fetch(
`/api/articles/${slug}?locale=${locale}`
)
const data = await response.json()
setArticle(data)
} catch (error) {
console.error('Failed to fetch article:', error)
} finally {
setLoading(false)
}
}

fetchArticle()
}, [slug, locale])

if (loading) return <p>Loading...</p>
if (!article) return <p>Article not found</p>

return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
)
}

Common Pitfalls and How to Avoid Them

1. Missing translations

When a translation is missing, your app might break or display raw keys:

jsx
// Solution: Create a withFallback wrapper for your translate function
function withFallback(t) {
return (key, options) => {
const translation = t(key, { ...options, returnNull: true })
if (translation === null) {
// Return the key itself or a fallback
return key.split('.').pop()
}
return translation
}
}

// Usage
const { t: originalT } = useTranslation()
const t = withFallback(originalT)

2. Managing large translation files

As your project grows, translation files can become unwieldy:

jsx
// Solution: Split translations into multiple namespaces
// In getStaticProps:
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'home', 'about', 'footer'])),
},
}
}

// In your component:
const { t } = useTranslation(['common', 'home'])

3. Forgetting to add new locale content

When adding new content, it's easy to forget to update all language files:

jsx
// Solution: Create a script that checks for missing translations
// check-translations.js
const fs = require('fs')
const path = require('path')

const locales = ['en', 'fr', 'es', 'de']
const baseLocale = 'en'

locales.forEach(locale => {
if (locale === baseLocale) return

const baseTranslations = JSON.parse(fs.readFileSync(
path.join(__dirname, `locales/${baseLocale}/common.json`), 'utf8'
))

const targetTranslations = JSON.parse(fs.readFileSync(
path.join(__dirname, `locales/${locale}/common.json`), 'utf8'
))

checkMissingKeys(baseTranslations, targetTranslations, '', locale)
})

function checkMissingKeys(base, target, prefix, locale) {
for (const key in base) {
const currentKey = prefix ? `${prefix}.${key}` : key
if (typeof base[key] === 'object' && base[key] !== null) {
checkMissingKeys(base[key], target[key] || {}, currentKey, locale)
} else if (!(key in target)) {
console.warn(`Missing translation for key "${currentKey}" in locale "${locale}"`)
}
}
}

Summary

Next.js makes internationalization accessible through its built-in i18n features and the ecosystem of libraries available. In this guide, we've covered:

  1. Setting up basic internationalization with Next.js's built-in features
  2. Creating and organizing translation files
  3. Building a language switcher
  4. Handling dates, numbers, and RTL languages
  5. Addressing SEO considerations for multi-language sites
  6. Fetching translations dynamically from an API
  7. Common pitfalls and their solutions

By implementing internationalization in your Next.js application, you open your product to a global audience and provide a better user experience for non-native speakers.

Additional Resources

Exercises

  1. Add a new language (e.g., Japanese) to the example project we built.
  2. Create a language detection feature that automatically selects the language based on the user's browser settings.
  3. Implement localized routes, where URL slugs are translated for each language.
  4. Build a dashboard to manage translations, showing completion percentages for each language.
  5. Use Next.js Image component with localized alt text for better accessibility across languages.

Remember that good internationalization is an ongoing process. As your application evolves, continue to refine your internationalization strategy and add new languages as needed.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)