The most surprising thing about Next.js internationalization is that your primary language isn’t special; it’s just another locale that needs explicit configuration.

Let’s see next-intl in action. Imagine a simple Next.js app with two languages, English (en) and Spanish (es).

pages/_app.tsx:

import { useRouter } from 'next/router';
import { NextIntlProvider } from 'next-intl';

function MyApp({ Component, pageProps }) {
  const router = useRouter();
  return (
    <NextIntlProvider
      // Load the messages for the current locale
      messages={pageProps.messages}
      // Optionally, set a default locale if the browser doesn't have one
      defaultLocale={router.defaultLocale}
      // The locale that will be used if no locale is found
      timeZone="America/New_York" // Example timezone
    >
      <Component {...pageProps} />
    </NextIntlProvider>
  );
}

// This function is needed to fetch messages for the initial page load
MyApp.getInitialProps = async (context) => {
  const { locale } = context.router;
  const messages = (await import(`../messages/${locale}.json`)).default;
  return {
    messages,
  };
};

export default MyApp;

pages/index.tsx:

import { useTranslations } from 'next-intl';
import { useRouter } from 'next/router';

function HomePage() {
  const t = useTranslations('Index'); // 'Index' is the namespace in your JSON
  const router = useRouter();

  const changeLocale = (locale) => {
    router.push(router.asPath, router.asPath, { locale });
  };

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('description')}</p>
      <button onClick={() => changeLocale('en')}>English</button>
      <button onClick={() => changeLocale('es')}>Español</button>
    </div>
  );
}

export default HomePage;

messages/en.json:

{
  "Index": {
    "welcome": "Welcome to our app!",
    "description": "This is a multi-language application."
  }
}

messages/es.json:

{
  "Index": {
    "welcome": "¡Bienvenido a nuestra aplicación!",
    "description": "Esta es una aplicación multilingüe."
  }
}

And in next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  i18n: {
    // List of all available locales
    locales: ['en', 'es'],
    // The default locale to use if no locale is detected
    defaultLocale: 'en',
    // Optional: domain-specific locales
    // domains: [
    //   {
    //     domain: 'example.com',
    //     defaultLocale: 'en',
    //   },
    //   {
    //     domain: 'example.es',
    //     defaultLocale: 'es',
    //   },
    // ],
  },
};

module.exports = nextConfig;

This setup allows users to switch between languages, and Next.js handles routing based on the locale. When you visit /, it defaults to English. If you switch to Spanish, the URL might become /es/ (depending on your routing strategy, which can be configured in next.config.js). The useTranslations hook then fetches the correct strings from the corresponding JSON file.

The problem next-intl solves is providing a robust and integrated way to manage translations within a Next.js application. It abstracts away the complexities of loading locale-specific messages, managing locale switching, and even handling complex scenarios like timezone-aware formatting. It leverages Next.js’s built-in i18n routing capabilities but provides a more developer-friendly API for accessing translations. Internally, it uses a context provider to make translation functions available throughout your component tree and dynamically loads message files based on the current locale.

The core levers you control are the locales and defaultLocale in next.config.js, which define the available languages and the fallback. The structure of your message JSON files (e.g., messages/[locale].json) and the namespaces you use with useTranslations (e.g., useTranslations('Namespace')) are also critical. You can also configure routing strategies, such as prefixing URLs with the locale (e.g., /en/about, /es/about) or using subdomains.

Most people don’t realize that next-intl can automatically infer the correct timezone for a user if you provide a timeZone prop to NextIntlProvider and then use the useNow hook. This allows for locale-aware date and time formatting that respects the user’s local context without requiring manual timezone detection.

The next concept you’ll likely encounter is how to handle dynamic routes and nested routes with internationalization, ensuring that parameters and paths are correctly translated and routed across different locales.

Want structured learning?

Take the full Nextjs course →