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:
- Sub-path routing:
/en/about
,/fr/about
- 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:
/** @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
):
{
"greeting": "Hello, world!",
"description": "Welcome to our Next.js application",
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
}
}
French (/locales/fr/common.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:
npm install next-i18next react-i18next i18next
Now, create a next-i18next.config.js
file in your project root:
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'es', 'de'],
},
localePath: './locales',
}
Then, create a useTranslation.js
hook:
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:
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:
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:
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:
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:
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:
.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:
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:
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:
// 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:
// 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:
// 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:
- Setting up basic internationalization with Next.js's built-in features
- Creating and organizing translation files
- Building a language switcher
- Handling dates, numbers, and RTL languages
- Addressing SEO considerations for multi-language sites
- Fetching translations dynamically from an API
- 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
- Next.js i18n Documentation
- next-i18next library
- MDN Intl API Reference
- react-intl library - Another popular i18n library
Exercises
- Add a new language (e.g., Japanese) to the example project we built.
- Create a language detection feature that automatically selects the language based on the user's browser settings.
- Implement localized routes, where URL slugs are translated for each language.
- Build a dashboard to manage translations, showing completion percentages for each language.
- 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! :)