
When Your Next.js App Needs to Speak Every Language: Diving into next-intl
Internationalization often feels like an afterthought until your product manager drops the 'global market' bomb. Suddenly, every string, date, and currency needs to adapt. While Next.js provides some primitives, a robust i18n solution is crucial, and next-intl makes this surprisingly elegant.
by Sunil Band
The Silent Killer of Global Reach
You've built a killer app. It's fast, slick, and your users love it. Then the business wants to expand, maybe into Germany, then Japan, then Brazil. Suddenly, every hardcoded string in your component tree, every date format, every currency display becomes a liability. You start thinking about if (locale === 'de') { ... }, and your beautiful codebase begins to sprout an ugly thicket of conditional logic.
Internationalization (i18n) isn't just about translating text. It's about adapting your entire user experience to different cultures, languages, and regions. This includes pluralization rules, date and time formatting, number and currency representation, and even text direction. Doing it manually is a nightmare. Doing it poorly means alienating half your potential user base. That's where a library like next-intl becomes indispensable for Next.js applications.
Next-intl isn't just a wrapper around react-intl or a basic string replacement utility. It's deeply integrated with Next.js's App Router, offering features like static generation (SSG) of translations, server-side rendering (SSR) of localized content, and efficient client-side hydration, all while maintaining excellent performance characteristics. It handles the complexities so you can focus on building features, not managing JSON files.
Why next-intl and not just react-intl?
If you've done i18n in React before, you're probably familiar with react-intl or i18next. They're powerful libraries, but they're client-side focused. When you bring Next.js into the picture, especially with the App Router and its server-first approach, you need something that understands the server/client boundary and optimizes for it.
Next-intl shines here because it's built for Next.js. It leverages the framework's capabilities to pre-render localized content on the server, minimizing layout shifts and ensuring that search engines see your localized content. This is crucial for SEO and perceived performance. Instead of fetching translations client-side after the initial render, next-intl ensures they're already there, baked into the HTML.
Setting up next-intl
Let's walk through a basic setup. I'm assuming you're using the Next.js App Router, because that's where next-intl really flexes its muscles. We'll start with a minimal next.config.js and then build out our internationalized routes.
First, install the package:
npm install next-intlNext, we need to configure our next.config.js to tell Next.js about our supported locales and how to handle routing. This is where next-intl integrates with Next.js's i18n routing capabilities.
// next.config.js
const withNextIntl = require('next-intl/plugin')();
/** @type {import('next').NextConfig} */
const nextConfig = {
// Other Next.js config options...
};
module.exports = withNextIntl(nextConfig);This withNextIntl plugin automatically handles much of the boilerplate for you. Now, let's define our locales and message files. I usually put these in a messages directory at the root of my project.
// messages/en.json
{
"Index": {
"title": "Welcome to my app",
"greeting": "Hello {name}!",
"count": "{count, plural, one {You have # message} other {You have # messages}}"
},
"About": {
"description": "This is an internationalized application."
}
}// messages/de.json
{
"Index": {
"title": "Willkommen in meiner App",
"greeting": "Hallo {name}!",
"count": "{count, plural, one {Sie haben # Nachricht} other {Sie haben # Nachrichten}}"
},
"About": {
"description": "Dies ist eine internationalisierte Anwendung."
}
}Notice the structured JSON keys. This isn't strictly required, but it helps organize messages by page or component, preventing key collisions and making maintenance easier. The plural syntax is standard ICU message format, which next-intl supports out of the box, handling complex pluralization rules for different languages.
Creating our i18n.ts and middleware.ts
For next-intl to work with the App Router, you need an i18n.ts configuration file and a middleware.ts file to intercept requests and determine the locale.
// i18n.ts
import { getRequestConfig } from 'next-intl/server';
// We can use an environment variable or a config file to define supported locales
const locales = ['en', 'de'];
export default getRequestConfig(async ({ locale }) => {
// Validate that the incoming locale is one of our supported locales
if (!locales.includes(locale as any)) {
// This is important for security and stability
throw new Error(`Unsupported locale: ${locale}`);
}
// Return the messages for the current locale
return {
messages: (await import(`./messages/${locale}.json`)).default
};
});This i18n.ts file dynamically loads the correct message file based on the detected locale. getRequestConfig is a server-side function, meaning this message loading happens once per request on the server, and the messages are then passed down to your components.
Now for middleware.ts. This file is crucial for routing and locale detection.
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// A list of all locales that are supported in this application
locales: ['en', 'de'],
// Used when no locale matches
defaultLocale: 'en',
// This setting ensures that the default locale (e.g., 'en') is also prefixed in the URL
// e.g., /en/about instead of just /about
localePrefix: 'always'
});
export const config = {
// Match only internationalized pathnames
matcher: [
'/', // Match all routes
'/((?!api|_next|.*\..*).*)' // Exclude `_next`, `api`, static files, etc.
]
};The createMiddleware function from next-intl handles detecting the locale from the URL, Accept-Language header, or a cookie, and then redirects if necessary. The localePrefix: 'always' is a strong recommendation for SEO and explicit routing. It means /about will redirect to /en/about or /de/about based on detection.
Using translations in your components
With the setup complete, let's create a root layout and a simple page to use our translations. All pages and layouts in your app directory will be wrapped by the root layout, making next-intl's NextIntlClientProvider available.
```typescript jsx
// app/[locale]/layout.tsx
import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
// Import our i18n config that determines supported locales and loads messages
import i18nConfig from '../../i18n'; // Adjust path if necessary
// Function to generate static params for all supported locales
// This enables static generation of localized pages
export function generateStaticParams() {
return i18nConfig.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
let messages;
try {
// Load messages using our i18n configuration
messages = (await import(../../messages/${locale}.json)).default;
} catch (error) {
// If messages for a locale are not found, return a 404
notFound();
}
return (
<html lang={locale}>
<body>
{/* Provide messages to all client components under this layout */}
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Notice the `generateStaticParams` function. This is critical for Next.js to know *which* locale routes to pre-render at build time, giving you excellent performance characteristics. For example, if you have `locales: ['en', 'de']`, Next.js will build `/en/*` and `/de/*` versions of all your pages.
Now, let's create a simple page (`app/[locale]/page.tsx`) that uses these translations:
```typescript jsx
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function IndexPage() {
// The `useTranslations` hook gets translations from the nearest NextIntlClientProvider
const t = useTranslations('Index'); // 'Index' refers to the key in en.json/de.json
return (
<div>
<h1>{t('title')}</h1>
<p>{t('greeting', { name: 'Sunil' })}</p>
<p>{t('count', { count: 1 })}</p> {/* Example with pluralization */}
<p>{t('count', { count: 5 })}</p>
<Link href="/about">
Go to About page
</Link>
</div>
);
}And an app/[locale]/about/page.tsx:
```typescript jsx
// app/[locale]/about/page.tsx
import { useTranslations } from 'next-intl';
export default function AboutPage() {
const t = useTranslations('About');
return (
<div>
<h1>About Us</h1>
<p>{t('description')}</p>
</div>
);
}
What's happening here? `useTranslations` is a client component hook. `NextIntlClientProvider` makes the messages available to all client components within its scope. When Next.js renders this on the server, the `messages` object provided to `NextIntlClientProvider` is serialized and sent to the client. This means the client doesn't need to re-fetch translations after hydration; they're already present.
For server components, you use `getTranslations` directly:
```typescript jsx
// app/[locale]/server-component-example/page.tsx
import { getTranslations } from 'next-intl/server';
// This is a Server Component
export default async function ServerComponentExample() {
// getTranslations is a server-side only function
const t = await getTranslations('Index');
return (
<div>
<h2>{t('title')} (from Server Component)</h2>
<p>{t('greeting', { name: 'Server-side Sunil' })}</p>
</div>
);
}This separation is powerful. Server Components get their translations before the HTML is streamed, ensuring full localization without client-side fetching. Client Components hydrate with pre-loaded translations, reducing flickers and improving user experience.
Trade-offs and Gotchas
No solution is perfect, and next-intl has its nuances:
- Strict File Structure: The
i18n.tsandmiddleware.tsfiles, along with themessagesdirectory structure, are fairly opinionated. While this enforces consistency, it can feel restrictive if you have an existing i18n setup that doesn't align. -
useTranslationsis for Client Components: Remember, you cannot calluseTranslationsdirectly in a Server Component. For Server Components, you mustawait getTranslations(). This distinction is fundamental to Next.js's App Router model and something you'll need to remember. - Locale Switching: While
next-intlhandles locale detection and routing well, building a robust locale switcher UI (e.g., a dropdown in your header) requires careful implementation. You'll typically usenext/linkto navigate to the new locale path, e.g.,/de/current-page-path. - Learning Curve: If you're new to the Next.js App Router and i18n, there's a double learning curve. Understanding how server and client components interact with
next-intl's hooks and utilities is key.
Despite these points, next-intl offers a coherent and performant story for internationalization within the Next.js App Router. The benefits of server-side message loading and static generation often outweigh the initial learning investment.
Wrapping up
Internationalization isn't just a feature; it's a fundamental requirement for any application aiming for a global audience. next-intl provides a robust, performant, and deeply integrated solution for Next.js App Router applications, moving beyond basic string replacement to offer full locale awareness, pluralization, and efficient server-side rendering.
My advice? Don't wait until your product goes live in ten countries to think about i18n. Start with next-intl early in your Next.js project. Clone their official example repository github.com/amannn/next-intl/tree/main/examples/example-app-router, run it locally, and play around with adding a new language or a complex pluralization rule. You'll quickly see how much heavy lifting it does for you, letting you focus on the features that actually matter to your users, no matter where they are.

When Your Frontend Needs to Process Files, Not Just Upload Them: Diving into WebAssembly
Sending user-generated files to a backend for processing often feels like the default, but it's not always the best solution. We'll explore how WebAssembly lets us handle complex file operations directly in the browser, boosting privacy and performance.

When Your React App Needs a 3D Dimension: Embracing React Three Fiber
Integrating 3D experiences into web applications used to be a mess of imperative Three.js code. React Three Fiber changes that, letting you build complex 3D scenes with familiar React components and state management. It's a game-changer for interactive visualizations and immersive UIs.

When Your React State Is Lying to You: Derived State Done Right
It's easy to fall into the trap of syncing a computed value into `useState`, leading to stale UI, extra renders, and subtle bugs. Let's fix derived state.


















