How to Translate a Next.js App: Complete Guide

Learn how to localize your Next.js app using next-intl or next-i18next. This comprehensive guide covers routing, string translation, pluralization, date formatting, and best practices for Ne

  • date icon

    Monday, Nov 17, 2025

How to Translate a Next.js App: Complete Guide

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:

  1. Built-in i18n routing - Automatic locale detection and routing
  2. next-intl - Modern library built for Next.js App Router
  3. 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:

  1. next-intl (Recommended for App Router) - Modern, built for Next.js 13+
  2. next-i18next (For Pages Router) - Popular, well-documented
  3. 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:

View AZbox Plans and Pricing

Blog

Latest Posts

Discover our latest articles and updates.

Why Translations Have Always Been a Problem in Software Development
date icon

Sunday, Dec 21, 2025

Why Translations Have Always Been a Problem in Software Development

For decades, software developers have struggled with translations and localization. What should be a straightforward pro

Read More
How to Translate a Flutter App with Azbox: Complete Guide
date icon

Saturday, Dec 20, 2025

How to Translate a Flutter App with Azbox: Complete Guide

Translating your Flutter app is essential for reaching a global audience. Azbox provides a powerful Flutter SDK that sim

Read More
Android Localization: Complete Guide to Expanding Your App to New Markets
date icon

Saturday, Dec 13, 2025

Android Localization: Complete Guide to Expanding Your App to New Markets

Android runs on many devices, in many regions. To reach the majority of users, make sure your app handles text, audio fi

Read More
Call to action background

Start Global Growth Today

Join hundreds of successful companies already using AZbox to reach customers worldwide. Start with a free trial, no credit card required.

Get Started - It's Free