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
/enand/arseparately) - Sharable URLs containing the language
- No cookies required
Cons
- More complex routing
- URLs become longer
- Changing locale means redirecting to another URL
B. Locale in Cookies (Recommended for Most Dashboards / SPAs)
In this method, the locale is saved inside a browser cookie, not the route.
Why Cookies?
- Cleaner URLs (no
/enor/ardirectories) - 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-intl3. 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.jsonExample:
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 AM10.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.jsonBut inside the file, everything is professionally structured using namespaces.
11.1. Recommended Structure
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
| Feature | Route-Based Locale | Cookies-Based Locale |
|---|---|---|
| SEO | ✔ Better | ❌ Not SEO-focused |
| Clean URLs | ❌ No (/en/..) | ✔ Yes |
| Redirects Needed | ✔ Yes | ❌ No |
| Best For | Blogs, marketing websites | Dashboards, SPAs, apps |