Translating your Next.js app is essential for reaching a global audience. Next.js provides excellent built-in support for internationalization (i18n) through routing and can be enhanced with libraries like next-intl or next-i18next. This guide will walk you through the process of translating a Next.js app.
Understanding Next.js Localization
Next.js supports internationalization through:
- Built-in i18n routing - Automatic locale detection and routing
- next-intl - Modern library built for Next.js App Router
- next-i18next - Popular library for Pages Router
We’ll cover both approaches in this guide.
Method 1: Using next-intl (App Router - Recommended)
next-intl is the recommended solution for Next.js 13+ with App Router.
Step 1: Install Dependencies
npm install next-intl
Or with yarn:
yarn add next-intl
Step 2: Create Translation Files
Create a messages directory in your project root:
messages/en.json (English - default):
{
"app": {
"welcome": "Welcome to our app!",
"title": "My Next.js App"
},
"button": {
"submit": "Submit",
"cancel": "Cancel",
"delete": "Delete"
},
"error": {
"network": "Network error. Please try again.",
"notFound": "Page not found"
},
"items": {
"count": "{count, plural, =0 {No items} one {# item} other {# items}}"
},
"user": {
"greeting": "Hello, {name}! You have {count, plural, =0 {no messages} one {# message} other {# messages}}."
}
}
messages/es.json (Spanish):
{
"app": {
"welcome": "¡Bienvenido a nuestra aplicación!",
"title": "Mi Aplicación Next.js"
},
"button": {
"submit": "Enviar",
"cancel": "Cancelar",
"delete": "Eliminar"
},
"error": {
"network": "Error de red. Por favor, inténtalo de nuevo.",
"notFound": "Página no encontrada"
},
"items": {
"count": "{count, plural, =0 {No hay elementos} one {# elemento} other {# elementos}}"
},
"user": {
"greeting": "¡Hola, {name}! Tienes {count, plural, =0 {no hay mensajes} one {# mensaje} other {# mensajes}}."
}
}
messages/fr.json (French):
{
"app": {
"welcome": "Bienvenue dans notre application!",
"title": "Mon Application Next.js"
},
"button": {
"submit": "Soumettre",
"cancel": "Annuler",
"delete": "Supprimer"
},
"error": {
"network": "Erreur réseau. Veuillez réessayer.",
"notFound": "Page non trouvée"
},
"items": {
"count": "{count, plural, =0 {Aucun élément} one {# élément} other {# éléments}}"
},
"user": {
"greeting": "Bonjour, {name}! Vous avez {count, plural, =0 {aucun message} one {# message} other {# messages}}."
}
}
Step 3: Configure next-intl
Create i18n.ts in your project root:
i18n.ts:
import { getRequestConfig } from 'next-intl/server';
import { notFound } from 'next/navigation';
export const locales = ['en', 'es', 'fr'] as const;
export const defaultLocale = 'en' as const;
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`./messages/${locale}.json`)).default
};
});
Step 4: Update next.config.js
next.config.js:
const createNextIntlPlugin = require('next-intl/plugin');
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = withNextIntl(nextConfig);
Step 5: Create Middleware
Create middleware.ts in your project root:
middleware.ts:
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from './i18n';
export default createMiddleware({
locales,
defaultLocale,
localePrefix: 'always' // or 'as-needed'
});
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};
Step 6: Update App Router Structure
Restructure your app directory to include [locale]:
app/
[locale]/
layout.tsx
page.tsx
about/
page.tsx
app/[locale]/layout.tsx:
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { locales } from '@/i18n';
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
if (!locales.includes(locale as any)) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
app/[locale]/page.tsx:
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations();
return (
<div>
<h1>{t('app.welcome')}</h1>
<p>{t('app.title')}</p>
</div>
);
}
Step 7: Use Translations in Components
Server Components:
import { useTranslations } from 'next-intl';
export default function ServerComponent() {
const t = useTranslations();
return <h1>{t('app.welcome')}</h1>;
}
Client Components:
'use client';
import { useTranslations } from 'next-intl';
export default function ClientComponent() {
const t = useTranslations();
return <h1>{t('app.welcome')}</h1>;
}
Step 8: String Interpolation
import { useTranslations } from 'next-intl';
export default function UserGreeting({ name, messageCount }: { name: string; messageCount: number }) {
const t = useTranslations('user');
return (
<p>
{t('greeting', { name, count: messageCount })}
</p>
);
}
Step 9: Pluralization
import { useTranslations } from 'next-intl';
export default function ItemCount({ count }: { count: number }) {
const t = useTranslations('items');
return <p>{t('count', { count })}</p>;
}
Step 10: Format Dates and Numbers
import { useTranslations, useFormatter } from 'next-intl';
export default function DateDisplay({ date }: { date: Date }) {
const format = useFormatter();
return (
<p>
{format.dateTime(date, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})}
</p>
);
}
export default function NumberDisplay({ number }: { number: number }) {
const format = useFormatter();
return (
<div>
<p>Number: {format.number(number)}</p>
<p>Currency: {format.number(number, { style: 'currency', currency: 'USD' })}</p>
</div>
);
}
Step 11: Language Switcher
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useLocale } from 'next-intl';
export default function LanguageSwitcher() {
const router = useRouter();
const pathname = usePathname();
const locale = useLocale();
const switchLocale = (newLocale: string) => {
const newPathname = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPathname);
};
return (
<div>
<button onClick={() => switchLocale('en')}>English</button>
<button onClick={() => switchLocale('es')}>Español</button>
<button onClick={() => switchLocale('fr')}>Français</button>
</div>
);
}
Method 2: Using next-i18next (Pages Router)
For Next.js Pages Router, use next-i18next.
Step 1: Install Dependencies
npm install next-i18next react-i18next i18next
Step 2: Create Translation Files
public/locales/en/common.json:
{
"welcome": "Welcome to our app!",
"button": {
"submit": "Submit",
"cancel": "Cancel"
}
}
public/locales/es/common.json:
{
"welcome": "¡Bienvenido a nuestra aplicación!",
"button": {
"submit": "Enviar",
"cancel": "Cancelar"
}
}
Step 3: Configure next-i18next
next-i18next.config.js:
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
},
localePath: './public/locales',
};
next.config.js:
const { i18n } = require('./next-i18next.config');
module.exports = {
i18n,
};
Step 4: Create Custom App
pages/_app.js:
import { appWithTranslation } from 'next-i18next';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default appWithTranslation(MyApp);
Step 5: Use Translations in Pages
pages/index.js:
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
export default function HomePage() {
const { t } = useTranslation('common');
return <h1>{t('welcome')}</h1>;
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
},
};
}
Method 3: Built-in Next.js i18n Routing
Next.js has built-in i18n support for Pages Router.
Step 1: Configure next.config.js
next.config.js:
module.exports = {
i18n: {
locales: ['en', 'es', 'fr'],
defaultLocale: 'en',
localeDetection: true, // Automatically detect user's locale
},
};
Step 2: Create Translation Files
locales/en.json:
{
"welcome": "Welcome to our app!"
}
locales/es.json:
{
"welcome": "¡Bienvenido a nuestra aplicación!"
}
Step 3: Use in Pages
pages/index.js:
import { useRouter } from 'next/router';
import translations from '../locales';
export default function HomePage() {
const router = useRouter();
const { locale } = router;
const t = translations[locale];
return <h1>{t.welcome}</h1>;
}
Best Practices
1. Use Descriptive Translation Keys
Bad:
{
"msg1": "Submit"
}
Good:
{
"button": {
"submit": "Submit"
}
}
2. Organize by Feature
{
"auth": {
"login": "Login",
"logout": "Logout"
},
"dashboard": {
"title": "Dashboard"
}
}
3. Use Namespaces
For large apps, split translations into namespaces:
next-i18next:
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'auth', 'dashboard'])),
},
};
}
next-intl:
const t = useTranslations('auth');
4. Handle Missing Translations
const t = useTranslations();
const text = t('some.key', { defaultValue: 'Default text' });
5. SEO Considerations
Set proper lang attribute and hreflang tags:
app/[locale]/layout.tsx:
export default function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
return (
<html lang={locale}>
<head>
<link rel="alternate" hrefLang="en" href="/en" />
<link rel="alternate" hrefLang="es" href="/es" />
<link rel="alternate" hrefLang="fr" href="/fr" />
</head>
<body>{children}</body>
</html>
);
}
6. Static Generation with Locales
Generate static pages for all locales:
next-intl:
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
next-i18next:
export async function getStaticPaths() {
return {
paths: locales.map((locale) => ({ params: { locale } })),
fallback: false,
};
}
Common Pitfalls
1. Not Configuring Middleware
For App Router with next-intl, always create middleware:
// middleware.ts is required
2. Forgetting Locale in URL Structure
Make sure your routes include locale:
/en/about
/es/about
/fr/about
3. Not Handling Client/Server Components
Use 'use client' for client components:
'use client';
import { useTranslations } from 'next-intl';
4. Hardcoding Strings
Bad:
<h1>Welcome</h1>
Good:
const t = useTranslations();
<h1>{t('app.welcome')}</h1>
Advanced: Dynamic Language Switching
next-intl:
'use client';
import { useRouter, usePathname } from 'next/navigation';
import { useLocale } from 'next-intl';
export function LanguageSwitcher() {
const router = useRouter();
const pathname = usePathname();
const locale = useLocale();
const changeLocale = (newLocale: string) => {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPath);
router.refresh();
};
return (
<select value={locale} onChange={(e) => changeLocale(e.target.value)}>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
);
}
Advanced: RTL Support
import { useLocale } from 'next-intl';
import { useEffect } from 'react';
export default function RTLHandler() {
const locale = useLocale();
const isRTL = ['ar', 'he', 'fa'].includes(locale);
useEffect(() => {
document.documentElement.dir = isRTL ? 'rtl' : 'ltr';
document.documentElement.lang = locale;
}, [locale, isRTL]);
return null;
}
Advanced: TypeScript Support
types/next-intl.d.ts:
type Messages = typeof import('./messages/en.json');
declare global {
interface IntlMessages extends Messages {}
}
Conclusion
Localizing your Next.js app can be done through multiple approaches:
- next-intl (Recommended for App Router) - Modern, built for Next.js 13+
- next-i18next (For Pages Router) - Popular, well-documented
- Built-in i18n routing (Pages Router) - Simple but limited
Choose the method that best fits your Next.js version and requirements. By following these practices, you’ll create an app that provides a native experience for users worldwide, significantly expanding your potential user base.
Streamline Your Next.js Localization Workflow
Managing translations for multiple languages can become complex as your app grows. Consider using a translation management platform to:
- Collaborate with translators
- Keep translations in sync with your codebase
- Automate the translation workflow
- Maintain consistency across all languages
- Generate translation files automatically
- Integrate with your CI/CD pipeline
Ready to take your Next.js app global? Explore AZbox’s localization platform and streamline your translation workflow: