Next.js Internationalization: Setup i18n in 5 Minutes

Next.js is one of the most popular React frameworks for building production-grade web applications, but adding multilingual support introduces unique challenges. Next.js internationalization (i18n) must account for server-side rendering, static generation, middleware-based routing, and SEO in ways that pure client-side React apps do not.

This guide walks you through everything you need to set up i18n in a Next.js project: routing strategies, step-by-step implementation with the App Router, dynamic route handling, SEO best practices, and a detailed comparison of next-intl, next-i18next, and Lovalingo.

Why Next.js Needs Special i18n Handling

React apps running entirely in the browser can swap languages on the fly because the client controls everything. Next.js is different. It renders pages on the server, generates static HTML at build time, and hydrates on the client. Each of these stages affects how translations are loaded and delivered.

SSR vs. CSR Implications

With server-side rendering, the HTML sent to the browser must already contain the correct translations. If you rely on client-side language detection alone, search engines will only see the default language, and users will experience a flash of untranslated content before JavaScript kicks in. This means your translation dictionaries must be available at request time on the server, not loaded asynchronously in the browser.

Static site generation (SSG) adds another layer. When you use generateStaticParams, you need to pre-render every page for every locale at build time. This multiplies the number of generated pages but produces the fastest possible multilingual experience.

App Router vs. Pages Router

The Pages Router had a built-in i18n configuration in next.config.js that handled locale detection, redirects, and prefixed routes automatically. The App Router removed this built-in feature. Instead, you implement locale routing through middleware and a [locale] dynamic segment in your folder structure. This gives you more control but requires more setup.

SEO Considerations

Server-rendered multilingual content is a significant SEO advantage. Search engines can crawl each locale as a distinct URL, index the content in the correct language, and serve the right version to users. This requires proper hreflang tags, locale-specific canonical URLs, and a localized sitemap, all of which must be generated server-side.

Next.js i18n Routing: Path-Based vs. Subdomain

Before writing any code, you need to choose a routing strategy for your locales. There are two main approaches.

Path-Based Routing

Path-based routing uses a locale prefix in the URL path:

  • example.com/en/about
  • example.com/fr/about
  • example.com/de/about

This is the most common approach and the easiest to set up with Next.js. It keeps all locales on the same domain, shares cookies and sessions, and simplifies deployment.

Subdomain Routing

Subdomain routing uses a separate subdomain for each locale:

  • en.example.com/about
  • fr.example.com/about
  • de.example.com/about

This approach is sometimes used for country-specific sites but adds DNS and hosting complexity.

Comparison

| Aspect | Path-Based | Subdomain | |--------|-----------|-----------| | Setup complexity | Low | High | | DNS configuration | None | Required per locale | | Cookie sharing | Automatic | Requires configuration | | SEO signals | Shared domain authority | Split domain authority | | Deployment | Single deployment | May require multiple | | Best for | Most projects | Country-specific sites |

For the vast majority of Next.js projects, path-based routing is the right choice. The rest of this guide assumes that approach.

Step-by-Step: Adding i18n to a Next.js App

Here is a complete implementation using the App Router with middleware for locale detection.

1. Define Your Supported Locales

Create a configuration file to keep locale settings in one place:

// src/i18n/config.ts
export const locales = ['en', 'fr', 'de', 'es'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';

2. Create the Middleware

The middleware intercepts every request, detects the user's preferred locale, and redirects to the correct prefixed path:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { locales, defaultLocale } from './src/i18n/config';
 
function getLocale(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language') || '';
  const preferred = acceptLanguage.split(',')[0]?.split('-')[0];
  return locales.includes(preferred as any) ? preferred : defaultLocale;
}
 
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // Check if the pathname already has a locale prefix
  const hasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );
 
  if (hasLocale) return NextResponse.next();
 
  // Redirect to the detected locale
  const locale = getLocale(request);
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}
 
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

3. Set Up the Folder Structure

Wrap your entire route tree inside a [locale] dynamic segment:

app/
  [locale]/
    layout.tsx
    page.tsx
    about/
      page.tsx
    blog/
      [slug]/
        page.tsx

4. Create Translation Dictionaries

Store translations as simple JSON files organized by locale:

// dictionaries/en.json
{
  "nav": { "home": "Home", "about": "About", "blog": "Blog" },
  "home": {
    "title": "Welcome to Our Platform",
    "subtitle": "Build global products effortlessly"
  }
}
// dictionaries/fr.json
{
  "nav": { "home": "Accueil", "about": "A propos", "blog": "Blog" },
  "home": {
    "title": "Bienvenue sur Notre Plateforme",
    "subtitle": "Construisez des produits mondiaux sans effort"
  }
}

Create a loader function:

// src/i18n/dictionaries.ts
import type { Locale } from './config';
 
const dictionaries = {
  en: () => import('../../dictionaries/en.json').then((m) => m.default),
  fr: () => import('../../dictionaries/fr.json').then((m) => m.default),
  de: () => import('../../dictionaries/de.json').then((m) => m.default),
  es: () => import('../../dictionaries/es.json').then((m) => m.default),
};
 
export const getDictionary = async (locale: Locale) => dictionaries[locale]();

5. Use Translations in Server Components

In the App Router, server components can load dictionaries directly with await:

// app/[locale]/page.tsx
import { getDictionary } from '@/i18n/dictionaries';
import type { Locale } from '@/i18n/config';
 
export default async function HomePage({
  params,
}: {
  params: { locale: Locale };
}) {
  const dict = await getDictionary(params.locale);
 
  return (
    <main>
      <h1>{dict.home.title}</h1>
      <p>{dict.home.subtitle}</p>
    </main>
  );
}

6. Create a Client-Side useTranslation Hook

For client components that need translations, pass the dictionary via context:

// src/i18n/TranslationProvider.tsx
'use client';
 
import { createContext, useContext } from 'react';
 
const TranslationContext = createContext<Record<string, any>>({});
 
export function TranslationProvider({
  dictionary,
  children,
}: {
  dictionary: Record<string, any>;
  children: React.ReactNode;
}) {
  return (
    <TranslationContext.Provider value={dictionary}>
      {children}
    </TranslationContext.Provider>
  );
}
 
export function useTranslation() {
  const dict = useContext(TranslationContext);
 
  function t(key: string): string {
    return key.split('.').reduce((obj, k) => obj?.[k], dict) ?? key;
  }
 
  return { t };
}

Then wrap your layout:

// app/[locale]/layout.tsx
import { getDictionary } from '@/i18n/dictionaries';
import { TranslationProvider } from '@/i18n/TranslationProvider';
import type { Locale } from '@/i18n/config';
 
export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { locale: Locale };
}) {
  const dictionary = await getDictionary(params.locale);
 
  return (
    <html lang={params.locale}>
      <body>
        <TranslationProvider dictionary={dictionary}>
          {children}
        </TranslationProvider>
      </body>
    </html>
  );
}

That is the manual approach. It works, but it requires maintaining translation files, managing keys, and wiring up context providers.

Handling Dynamic Routes with Multiple Languages

Dynamic routes like blog posts need to generate pages for every combination of locale and slug.

generateStaticParams for Localized Pages

// app/[locale]/blog/[slug]/page.tsx
import { locales, type Locale } from '@/i18n/config';
import { getDictionary } from '@/i18n/dictionaries';
import { getBlogPost, getAllSlugs } from '@/lib/blog';
 
export async function generateStaticParams() {
  const slugs = await getAllSlugs();
 
  return locales.flatMap((locale) =>
    slugs.map((slug) => ({ locale, slug }))
  );
}
 
export default async function BlogPost({
  params,
}: {
  params: { locale: Locale; slug: string };
}) {
  const dict = await getDictionary(params.locale);
  const post = await getBlogPost(params.slug, params.locale);
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

This generates static pages at build time for every locale-slug combination, such as /en/blog/getting-started, /fr/blog/getting-started, /de/blog/getting-started, and so on.

SEO Best Practices for Multilingual Next.js Sites

Getting i18n routing to work is only half the battle. Search engines need explicit signals to understand your multilingual content.

generateMetadata per Locale

// app/[locale]/page.tsx
import type { Locale } from '@/i18n/config';
import { getDictionary } from '@/i18n/dictionaries';
 
export async function generateMetadata({
  params,
}: {
  params: { locale: Locale };
}) {
  const dict = await getDictionary(params.locale);
 
  return {
    title: dict.home.metaTitle,
    description: dict.home.metaDescription,
    alternates: {
      canonical: `https://example.com/${params.locale}`,
      languages: {
        en: 'https://example.com/en',
        fr: 'https://example.com/fr',
        de: 'https://example.com/de',
        'x-default': 'https://example.com/en',
      },
    },
  };
}

Hreflang Tags in Next.js

The alternates.languages field in generateMetadata automatically generates hreflang <link> tags in the <head>. Each hreflang tag tells search engines which URL serves which language:

<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en" />

For more detail on hreflang implementation, see our complete hreflang tags guide.

Localized Sitemaps

Create a sitemap route that includes every locale variant:

// app/sitemap.ts
import { locales } from '@/i18n/config';
 
export default function sitemap() {
  const baseUrl = 'https://example.com';
  const pages = ['', '/about', '/blog'];
 
  return locales.flatMap((locale) =>
    pages.map((page) => ({
      url: `${baseUrl}/${locale}${page}`,
      lastModified: new Date(),
      alternates: {
        languages: Object.fromEntries(
          locales.map((l) => [l, `${baseUrl}/${l}${page}`])
        ),
      },
    }))
  );
}

Canonical URLs

Every localized page should have a canonical URL pointing to itself, not to the default language version. This prevents search engines from treating localized pages as duplicate content.

Lovalingo vs. next-intl vs. next-i18next

Here is how the three main approaches compare for Next.js internationalization:

| Feature | next-intl | next-i18next | Lovalingo | |---------|----------|-------------|-----------| | Setup time | 30-60 min | 30-60 min | 5 min | | App Router support | Full | Limited | Full | | React Server Components | Yes | No | Yes | | Translation files | Required (JSON) | Required (JSON) | Not required | | Bundle size impact | ~15 KB | ~22 KB | ~8 KB | | Learning curve | Medium | Medium | Minimal | | SSR support | Yes | Yes (Pages Router) | Native | | Automatic translation | No | No | Yes (AI-powered) | | Hreflang generation | Manual | Manual | Automatic | | Pluralization | ICU format | Built-in rules | Automatic | | Translation management | External tools | External tools | Built-in dashboard |

When to Choose Each

Choose next-intl if you are building a new App Router project, have professional translators providing translation files, and want fine-grained control over message formatting with ICU syntax.

Choose next-i18next if you are maintaining a Pages Router project, already use i18next elsewhere in your stack, and need the extensive i18next plugin ecosystem for backends, caching, and namespaces.

Choose Lovalingo if you want the fastest possible setup, do not want to manage translation files or keys, need automatic AI-powered translation, and want built-in SEO features like hreflang generation and localized metadata. Lovalingo's native rendering approach means translations are resolved before React renders, eliminating layout shift and hydration mismatches entirely.

FAQ

Does Next.js have built-in i18n support?

The Pages Router has built-in i18n routing via the i18n config in next.config.js, supporting locale-prefixed paths and automatic locale detection. The App Router does not include built-in i18n routing, but you can implement it with middleware and a [locale] dynamic segment, or use libraries like next-intl or Lovalingo for a turnkey solution.

Which is better: next-intl or next-i18next?

next-intl is designed specifically for the App Router and React Server Components, making it the better choice for modern Next.js projects. next-i18next works best with the Pages Router and getServerSideProps/getStaticProps. If you want zero-config i18n without managing translation files, Lovalingo handles everything automatically.

How do I handle SEO for multilingual Next.js apps?

Use generateMetadata to create locale-specific titles, descriptions, and canonical URLs. Add hreflang link tags in your layout so search engines understand language relationships. Generate a localized sitemap with next-sitemap or a custom sitemap route. Lovalingo automates hreflang generation and localized metadata out of the box.

Can I add i18n to an existing Next.js project?

Yes. For the App Router, add a middleware.ts file for locale detection, wrap your routes in a [locale] dynamic segment, and load translation dictionaries per locale. Libraries like next-intl simplify migration. Lovalingo requires no refactoring at all: install the package, wrap your layout, and translations work immediately.

What's the fastest way to add translations to Next.js?

The fastest approach is Lovalingo. Install the package, wrap your root layout with the LovaLingoProvider, and your entire Next.js app is translated automatically with native rendering, SSR support, and SEO-ready hreflang tags. No translation files, no key extraction, no middleware configuration required.


Ready to add multilingual support to your Next.js app without the boilerplate? Get started with Lovalingo for Next.js and ship translations in minutes instead of days.

Related Guides

Ready to automate your i18n workflow?

Lovalingo translates your React & Next.js apps automatically with native rendering.

Try Lovalingo Free