Skip to Content

I18n in Next.js (Using next-intl)

Internationalization (i18n) allows your Next.js application to support multiple languages. Next.js offers two main ways to store and detect the user’s locale:


1. Ways to Store Locale (Route-Based vs Cookies-Based)

A. Locale in the Route (e.g., /en/products)

This method relies on the locale being part of the URL.

How it works

  • You wrap your entire app/ inside a dynamic segment:

    app/[locale]/
  • Next.js automatically injects the locale, and you can load messages based on it.

Pros

  • SEO-friendly (Google can index /en and /ar separately)
  • Sharable URLs containing the language
  • No cookies required

Cons

  • More complex routing
  • URLs become longer
  • Changing locale means redirecting to another URL

In this method, the locale is saved inside a browser cookie, not the route.

Why Cookies?

  • Cleaner URLs (no /en or /ar directories)
  • The locale persists between page visits
  • Works well for authenticated dashboards, admin panels, etc.
  • Easy to update without page reloads
  • No SEO concerns if your app isn’t content-focused

💡 We will use the cookie approach for the rest of this guide.


2. Install next-intl

npm install next-intl

3. Update next.config.js to use the next-intl Plugin

// next.config.ts import type { NextConfig } from 'next' import createNextIntlPlugin from 'next-intl/plugin' const nextConfig: NextConfig = { /* your Next config */ } const withNextIntl = createNextIntlPlugin() export default withNextIntl(nextConfig)

This enables next-intl to integrate automatically with the App Router.


4. Adding the next-intl Provider

Wrap your application with NextIntlClientProvider (usually in layout.tsx):

import { NextIntlClientProvider } from 'next-intl' export default function RootLayout({ children, params }: any) { return ( <html lang={currentLang}> <body> <NextIntlClientProvider>{children}</NextIntlClientProvider> </body> </html> ) }

This makes translation functions available anywhere in your components.


5. Defining Translation Messages

Create your translation files:

/messages ├─ ar.json └─ en.json

Example:

ar.json

{ "welcome": "أهلاً بك", "home": { "title": "الصفحة الرئيسية" } }

en.json

{ "welcome": "Welcome", "home": { "title": "Home Page" } }

6. Loading Messages on the Server (i18n/request.ts)

This ensures the correct language loads depending on the cookie:

// i18n/request.ts import { getRequestConfig } from 'next-intl/server' import { cookies } from 'next/headers' export default getRequestConfig(async () => { const store = await cookies() const locale = store.get('locale')?.value || 'ar' return { locale, messages: (await import(`@/messages/${locale}.json`)).default } })

7. Using the Translation Function

Client Component Usage

'use client' import { useTranslations } from 'next-intl' export default function Home() { const t = useTranslations('home') return <h1>{t('title')}</h1> }

Server Component Usage

import { getTranslations } from 'next-intl/server' export default async function Page() { const t = await getTranslations('home') return <h1>{t('title')}</h1> }

8. Changing the Locale (setLocale Helper)

You can easily switch languages by updating the cookie:

// app/actions/set-locale.ts 'use server' import { cookies } from 'next/headers' export async function setLocale(locale: string) { const store = await cookies() store.set('locale', locale, { path: '/', maxAge: 60 * 60 * 24 * 365 // 1 year }) }

Example usage:

'use client' import { setLocale } from '@/app/actions/set-locale' export function LanguageSwitcher() { return <button onClick={() => setLocale('en')}>Switch to English</button> }

9. Getting the Current Locale

Use the helper from next-intl:

import { getLocale } from 'next-intl/server' export default async function Page() { const locale = await getLocale() return <div>Current locale: {locale}</div> }

10. Passing prop to message

To pass dynamic values (props) into your translation message using next-intl, you use interpolation placeholders inside your JSON and pass the values when calling t().

10.1. Define a message with placeholders

Inside your translation JSON, use {variableName}:

{ "dashboard": { "welcome": "Welcome back, {name}", "newMessages": "You have {count} new messages", "price": "The price is {value} USD", "time": "The meeting starts at {time}" } }

This works in both en.json and ar.json.


10.2. Pass the values when calling the message

In your component:

import { useTranslations } from 'next-intl' export default function Dashboard() { const t = useTranslations('dashboard') return ( <> <h1>{t('welcome', { name: 'Tareq' })}</h1> <p>{t('newMessages', { count: 5 })}</p> <p>{t('price', { value: 120 })}</p> <p>{t('time', { time: '10:30 AM' })}</p> </> ) }

Example result

Welcome back, Tareq You have 5 new messages The price is 120 USD The meeting starts at 10:30 AM

10.3. Nested objects also work

{ "auth": { "otp": { "sentTo": "A code was sent to {phone}" } } }

Usage:

const t = useTranslations('auth.otp') t('sentTo', { phone: '+971 55 123 4567' })

10.4. Using pluralization

{ "notifications": "{count, plural, one {You have 1 notification} other {You have # notifications}}" }

Usage:

t('notifications', { count: 1 }) t('notifications', { count: 5 })

11. Professional Message Structure

You will have:

/messages ├─ en.json └─ ar.json

But inside the file, everything is professionally structured using namespaces.


Each translation file contains top-level groups:

{ "common": { ... }, "actions": { ... }, "errors": { ... }, "navbar": { ... }, "forms": { ... }, "auth": { ... }, "dashboard": { ... }, "validation": { ... }, "messages": { ... } }

This keeps everything in one file but clean, consistent, and scalable.


11.2. How to Use It

Example usage in the app

const t = useTranslations('actions') <button>{t('save')}</button>

Or nested:

const t = useTranslations('dashboard.stats') t('sales') // → "Sales"

11.3. Summary

✔ One file per locale (simple & clean) ✔ Organized into namespaces inside the file ✔ Easy to navigate ✔ Scalable for large apps ✔ Fully static and works perfectly with next-intl


12. Reference

https://next-intl.dev/docs/getting-started/app-router 


13. Summary

FeatureRoute-Based LocaleCookies-Based Locale
SEO✔ Better❌ Not SEO-focused
Clean URLs❌ No (/en/..)✔ Yes
Redirects Needed✔ Yes❌ No
Best ForBlogs, marketing websitesDashboards, SPAs, apps