The most surprising thing about setting up internationalization (i18n) in React with Next.js isn’t how complex it is, but how elegantly next-i18next abstracts away the boilerplate, letting you focus on the translations themselves.
Let’s see it in action with a minimal Next.js setup. Imagine we want to support English (en) and Spanish (es).
First, install the necessary packages:
npm install next-i18next react-i18next
# or
yarn add next-i18next react-i18next
Next, configure next-i18next in your next.config.js:
// next.config.js
const { i18n } = require('./next-i18n.config');
module.exports = {
i18n,
};
Create the next-i18n.config.js file:
// next-i18n.config.js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
localeDetection: false, // Recommended: Manage locale detection manually for clarity
},
};
Now, create your translation files. A common pattern is to have a public/locales directory, with subdirectories for each locale, containing JSON files for namespaces.
public/locales/en/common.json:
{
"welcome": "Welcome to our app!",
"changeLanguage": "Change language"
}
public/locales/es/common.json:
{
"welcome": "¡Bienvenido a nuestra aplicación!",
"changeLanguage": "Cambiar idioma"
}
You’ll also need a _app.js (or _app.tsx) to wrap your application with appWithTranslation:
// pages/_app.js
import { appWithTranslation } from 'next-i18next';
import '../styles/globals.css'; // Assuming you have global styles
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default appWithTranslation(MyApp);
Finally, use the useTranslation hook in your components:
// pages/index.js
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Link from 'next/link';
export default function Home() {
const { t, i18n } = useTranslation('common'); // 'common' is the namespace
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('changeLanguage')}</p>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('es')}>Español</button>
<p>
<Link href="/" locale="en">
<a>English Version</a>
</Link>
</p>
<p>
<Link href="/" locale="es">
<a>Spanish Version</a>
</Link>
</p>
</div>
);
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Other props
},
};
}
This setup defines your supported languages, where to find their translations, and how to load them. next-i18next handles the routing (/en/page, /es/page), server-side rendering (SSR) of translations for SEO and initial load performance, and client-side language switching.
The serverSideTranslations function is crucial. It fetches the translation files for the given locale and namespaces and makes them available to the page during server-side rendering. This ensures that the initial HTML sent to the browser already contains the translated content, improving perceived performance and SEO. Without it, the initial render would be in the default language, and the translations would only load client-side, leading to a flash of untranslated content.
The localeDetection flag in next-i18n.config.js is worth noting. When set to true (the default), Next.js attempts to detect the user’s preferred language from their browser’s Accept-Language header and cookies. While convenient, it can sometimes lead to unexpected redirects if not configured carefully. Setting it to false gives you explicit control over locale switching, often by using explicit links or buttons as shown in the example, which can be more predictable for users and easier to debug.
You might wonder about dynamic routes and translations. For example, if you have a page like pages/posts/[id].js, you’d pass the id to serverSideTranslations and then use t within the component, potentially translating a title like t('postTitle', { id }). The useTranslation hook and serverSideTranslations work seamlessly with dynamic routes, allowing you to fetch and apply translations based on the current route parameters.
One common pitfall is forgetting to include all necessary namespaces in serverSideTranslations. If a component on a page uses translations from a namespace not listed in serverSideTranslations for that page’s getStaticProps or getServerSideProps, those translations won’t be available, and t('key') will return the key itself. Always ensure every namespace used by a page and its child components is passed to serverSideTranslations.
The next challenge you’ll likely encounter is managing more complex translation scenarios, such as pluralization, interpolation, and context-specific translations within your application.