404 Not Found

404 Not Found


nginx

Internationalization (i18n) and Middleware: next-intl + RTL Layout

Internationalization isn't just about translation—it's a systematic engineering process involving routing, locale detection, and RTL layout.

1. What You'll Learn


2. The True Story of an International Product Manager

(1) Pain Point: 47 negative reviews from users in the Middle East

After Alice's TaskFlow went live, it received a large number of complaints from users in Saudi Arabia and the United Arab Emirates:

"The dashboard is still in English, the date format 07/06/2026 makes no sense to them (Saudi Arabia uses 06/07/2026), and the number 1,234.56 displays incorrectly in Arab countries. Worst of all—the page scrolls from right to left, and the entire layout is reversed."

Issue User Feedback Number of Users Affected
Page not translated into Arabic "Can't understand the menu" 47 negative reviews
Incorrect date format "The date is reversed" 32 complaints
Layout not optimized for RTL "The button is on the right" 28 complaints
Currency Symbol Error "Price Displayed in USD" 15 Complaints

(2) The next-intl + Middleware Solution

Use next-intl to provide an internationalization framework; the middleware automatically switches based on the browser's language.

TS
// middleware.ts — International Routing Core
import createMiddleware from 'next-intl/middleware'

export default createMiddleware({
  locales: ['zh', 'en', 'ja', 'ar'],
  defaultLocale: 'zh',
  localePrefix: 'always'       // URL Always include the region prefix
})

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)']
}

(3) Revenue

Dimension Before the redesign After the redesign
Supported Languages Chinese only 4 languages: Chinese, English, Japanese, and Arabic
Language Detection Manual Selection Auto-detect browser language
RTL Support ❌ Not Supported ✅ Full Mirror Layout
Date/Number Hard-coded Chinese format Intl API auto-formatting
Saudi Users NPS 32 78

3. Installing next-intl and Project Structure

(1) Installation and Directory Structure

BASH
npm install next-intl
100%
graph TB
    A[next-intl] --> B[i18n/request.ts<br/>Server Configuration]
    A --> C[messages/<br/>Translation Dictionary]
    C --> D[en.json<br/>English]
    C --> E[zh.json<br/>Chinese]
    C --> F[ja.json<br/>Japanese]
    C --> G[ar.json<br/>Arabic]
    A --> H[middleware.ts<br/>Language Detection + Rewrite]
    A --> I[app/[locale]/<br/>Dynamic Routing Segments]

    style A fill:#cce5ff
    style H fill:#d4edda
    style I fill:#fff3cd
File Purpose
i18n/request.ts Server-side i18n configuration entry point
messages/en.json English Dictionary
messages/ar.json Arabic Dictionary (with RTL Markup)
middleware.ts Automatic Language Detection + URL Rewriting
app/[locale]/ Dynamic Regional Routing Segment

(2) Initial Configuration

TS
// i18n/request.ts — next-intl Server Configuration
import { getRequestConfig } from 'next-intl/server'
import { hasLocale } from 'next-intl'
import { routing } from './routing'

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale
  const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default
  }
})
TS
// i18n/routing.ts — Sharing Router Configurations
import { defineRouting } from 'next-intl/routing'

export const routing = defineRouting({
  locales: ['zh', 'en', 'ja', 'ar'],
  defaultLocale: 'zh',
  localeDetection: true,
  localePrefix: 'as-needed'    // Omit the prefix for the default language
})

▶ Example: Complete Middleware Configuration

TS
// middleware.ts — Language Detection + Cookie Persistence
import createMiddleware from 'next-intl/middleware'
import { NextRequest, NextResponse } from 'next/server'

const intlMiddleware = createMiddleware({
  locales: ['zh', 'en', 'ja', 'ar'],
  defaultLocale: 'zh',
  localeDetection: true,
  localePrefix: 'always'
})

export default function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl

  // API / _next Do not process
  if (pathname.startsWith('/api') || pathname.startsWith('/_next') || pathname === '/favicon.ico') {
    return NextResponse.next()
  }

  return intlMiddleware(req)
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}

4. Middleware Routing Policies

(1) Three Routing Modes

100%
graph TB
    A[User Access] --> B{Middleware Testing}
    B --> C[Inspection Cookie<br/>next-intl-locale]
    B --> D[Inspect Accept-Language<br/>HTTP Header]
    B --> E[Default Language]

    C --> F[Selection Made → Usage Options]
    D --> G[First Visit → Detect Browser Language]
    E --> H[No matches found → Default Language]
    F --> I[URL Rewrite /{locale}/...]
    G --> I
    H --> I

    I --> J[Write Cookie<br/>Persistence Options]

    style B fill:#fff3cd
    style I fill:#d4edda
    style J fill:#cce5ff
Routing Mode URL Format localePrefix Applicable Scenarios
Always Prefix /en/about / /ar/about 'always' Equal display for all languages
On-Demand Prefix /about (default) / /en/about 'as-needed' Default Language SEO-Friendly
Never Prefix /about 'never' Cookie-driven (not recommended)
TS
// middleware.ts — Manual Control of Detection Logic
import { NextRequest, NextResponse } from 'next/server'
import { routing } from './i18n/routing'

const COOKIE_NAME = 'NEXT_LOCALE'

export default function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl
  const cookieLocale = req.cookies.get(COOKIE_NAME)?.value
  const acceptLanguage = req.headers.get('Accept-Language')?.split(',')[0]?.split('-')[0]
  const browserLocale = acceptLanguage && routing.locales.includes(acceptLanguage as any) ? acceptLanguage : null

  // Matching Languages
  const matchedLocale = cookieLocale || browserLocale || routing.defaultLocale

  // The path already has a language prefix
  const pathLocale = routing.locales.find((l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`)
  if (pathLocale) return NextResponse.next()

  // Rewrite URL and set Cookie
  const url = new URL(`/${matchedLocale}${pathname}`, req.url)
  const res = NextResponse.rewrite(url)
  res.cookies.set(COOKIE_NAME, matchedLocale, { maxAge: 60 * 60 * 24 * 365 })
  return res
}

export const config = { matcher: ['/((?!api|_next|favicon.ico).*)'] }
💡 Tip: Cookie persistence ensures that the user's language selection is retained when switching between pages. When the user manually switches languages, update the cookie value.

▶ Example: Language Switch Component

TSX
// components/LocaleSwitcher.tsx
'use client'
import { useLocale } from 'next-intl'
import { usePathname, useRouter } from '@/i18n/routing'
import { useTransition } from 'react'

const locales = [
  { code: 'zh', label: 'Chinese' },
  { code: 'en', label: 'English' },
  { code: 'ja', label: 'Japanese' },
  { code: 'ar', label: 'العربية' }
]

export default function LocaleSwitcher() {
  const locale = useLocale()
  const router = useRouter()
  const pathname = usePathname()
  const [isPending, startTransition] = useTransition()

  const switchLocale = (nextLocale: string) => {
    startTransition(() => {
      router.replace(pathname, { locale: nextLocale })
    })
  }

  return (
    <select
      value={locale}
      onChange={(e) => switchLocale(e.target.value)}
      disabled={isPending}
      style={{ padding: '0.25rem 0.5rem' }}
    >
      {locales.map((l) => (
        <option key={l.code} value={l.code}>{l.label}</option>
      ))}
    </select>
  )
}

5. Multilingual Dictionary Management

(1) Dictionary File Structure

JSON
// messages/zh.json
{
  "nav": {
    "home": "Home",
    "dashboard": "Dashboard",
    "projects": "Project",
    "settings": "Settings",
    "signIn": "Log In",
    "signOut": "Exit"
  },
  "home": {
    "title": "TaskFlow - Team Collaboration Platform",
    "subtitle": "Help 10,000+ Highly Effective Team Collaboration",
    "cta": "Get Started for Free"
  },
  "dashboard": {
    "welcome": "Welcome back,{name}",
    "stats": {
      "members": "{count} Famous members",
      "projects": "{count} projects",
      "tasks": "{count} A task"
    },
    "activity": "Recent Events"
  },
  "common": {
    "loading": "Loading......",
    "error": "An error occurred",
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete",
    "confirm": "Confirm"
  }
}
JSON
// messages/ar.json
{
  "nav": {
    "home": "الرئيسية",
    "dashboard": "لوحة التحكم",
    "projects": "المشاريع",
    "settings": "الإعدادات",
    "signIn": "تسجيل الدخول",
    "signOut": "تسجيل الخروج"
  },
  "home": {
    "title": "TaskFlow - منصة التعاون الجماعي",
    "subtitle": "نساعد أكثر من 10,000 فريق على العمل بكفاءة",
    "cta": "ابدأ مجاناً"
  },
  "dashboard": {
    "welcome": "مرحباً بعودتك، {name}",
    "stats": {
      "members": "{count} أعضاء",
      "projects": "{count} مشروعاً",
      "tasks": "{count} مهمة"
    },
    "activity": "النشاطات الأخيرة"
  },
  "common": {
    "loading": "جارٍ التحميل...",
    "error": "حدث خطأ",
    "save": "حفظ",
    "cancel": "إلغاء",
    "delete": "حذف",
    "confirm": "تأكيد"
  }
}

(2) Using Translations in Components

TSX
// Server Component:Usage t() Function
import { getTranslations } from 'next-intl/server'

export default async function HomePage() {
  const t = await getTranslations('home')

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
      <button>{t('cta')}</button>
    </div>
  )
}
TSX
// Client Component:useTranslations Hook
'use client'
import { useTranslations } from 'next-intl'

export default function DashboardHeader({ name }: { name: string }) {
  const t = useTranslations('dashboard')

  return (
    <h1>{t('welcome', { name })}</h1>
  )
}
💻 Output (Chinese): Welcome back, Alice 💻 Output (Arabic): مرحباً بعودتك، Alice

▶ Example: Translations containing complex numbers

TSX
// Dictionary
{
  "tasks": {
    "remaining": "{count, plural, =0 {No tasks} one {# A task} other {# A task}}"
  }
}
TSX
import { useTranslations } from 'next-intl'

export default function TaskCount({ count }: { count: number }) {
  const t = useTranslations('tasks')

  return <span>{t('remaining', { count })}</span>
}
💻 Output (count=0): No tasks 💻 Output (count=1): 1 task 💻 Output (count=5): 5 tasks

💡 Tip: next-intl Uses ICU MessageFormat syntax and supports advanced features such as plural forms, options, and number formatting.


6. generateStaticParams: Pre-generate multilingual pages

(1) Principles of Static Pre-generation

100%
graph LR
    A[generateStaticParams] --> B[Back locale List]
    B --> C[zh: /zh/blog/post-1]
    B --> D[en: /en/blog/post-1]
    B --> E[ja: /ja/blog/post-1]
    B --> F[ar: /ar/blog/post-1]
    C --> G[Generated during build: 4 HTML files]
    D --> G
    E --> G
    F --> G

    style A fill:#cce5ff
    style G fill:#d4edda
Language HTML File Path
Chinese out/zh/blog/post-1.html /zh/blog/post-1
English out/en/blog/post-1.html /en/blog/post-1
Japanese out/ja/blog/post-1.html /ja/blog/post-1
Arabic out/ar/blog/post-1.html /ar/blog/post-1

(2) The "locale" parameter in the root layout

TSX
// app/[locale]/layout.tsx — Root Layout Receive locale Parameters
import { NextIntlClientProvider } from 'next-intl'
import { getMessages, getTranslations } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { routing } from '@/i18n/routing'

type Props = { children: React.ReactNode; params: Promise<{ locale: string }> }

export default async function LocaleLayout({ children, params }: Props) {
  const { locale } = await params

  // Verification locale Is it valid?
  if (!routing.locales.includes(locale as any)) notFound()

  const messages = await getMessages()

  return (
    <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  )
}

▶ Example: Static Generation of Multilingual Blog Posts

TSX
// app/[locale]/blog/[slug]/page.tsx — Multilingual SSG
import { getTranslations } from 'next-intl/server'
import { routing } from '@/i18n/routing'

type Props = { params: Promise<{ locale: string; slug: string }> }

// Pre-generate article pages for all languages
export async function generateStaticParams() {
  const posts = await fetch('https://api.taskflow.io/blog/posts').then(r => r.json())

  return routing.locales.flatMap((locale) =>
    posts.map((post: { slug: string }) => ({ locale, slug: post.slug }))
  )
}

export async function generateMetadata({ params }: Props) {
  const { locale, slug } = await params
  const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())

  return {
    title: locale === 'en' ? post.title : post.titleLocalized[locale],
    alternates: {
      languages: Object.fromEntries(
        routing.locales.map((l) => [l, `https://taskflow.io/${l}/blog/${slug}`])
      )
    }
  }
}

export default async function BlogPostPage({ params }: Props) {
  const { locale, slug } = await params
  const t = await getTranslations('blog')

  return (
    <article>
      <h1>{t('title')}</h1>
      <p>Locale: {locale} | Slug: {slug}</p>
    </article>
  )
}

7. RTL Layout Support

(1) Differences Between RTL and LTR

100%
graph TB
    subgraph LTR Layout
        A1[Logo<br/>Left-aligned] --> A2[Navigation Bar<br/>From left to right]
        A2 --> A3[Sidebar<br/>Left-aligned]
        A3 --> A4[Content<br/>Keep to the right]
    end
    subgraph RTL Layout
        B1[Logo<br/>Keep to the right] --> B2[Navigation Bar<br/>From right to left]
        B2 --> B3[Sidebar<br/>Keep to the right]
        B3 --> B4[Content<br/>Left-aligned]
    end

    style B1 fill:#d4edda
    style B2 fill:#d4edda
    style B3 fill:#d4edda
Dimension LTR (Chinese/English/Japanese) RTL (Arabic)
Text Direction Left-to-Right Right-to-Left
Navigation Bar Logo on the left → Menu on the right Logo on the right → Menu on the left
Sidebar Left Right
Arrow icon Forward Forward
Card margin margin-left: auto margin-right: auto
Form Submit Button Bottom Right Bottom Left

(2) Tailwind CSS RTL Support

TSX
// app/[locale]/layout.tsx — RTL Testing and CSS Class Name
export default async function LocaleLayout({ children, params }: {
  children: React.ReactNode
  params: Promise<{ locale: string }>
}) {
  const { locale } = await params
  const isRtl = locale === 'ar'

  return (
    <html lang={locale} dir={isRtl ? 'rtl' : 'ltr'}>
      <body className={isRtl ? 'rtl' : 'ltr'}>
        {children}
      </body>
    </html>
  )
}
CSS
/* app/globals.css — RTL Basic Styles */
.ltr { direction: ltr; }
.rtl { direction: rtl; }

/* Navigation Bar Mirroring */
.rtl .nav-items { flex-direction: row-reverse; }
.rtl .sidebar { right: 0; left: auto; }
.rtl .main-content { margin-right: 260px; margin-left: 0; }

/* Icon Mirroring */
.rtl .icon-arrow::before { content: '←'; }
.ltr .icon-arrow::before { content: '→'; }

/* Spacing Reversal */
.rtl .ml-auto { margin-right: auto; margin-left: 0; }
.rtl .mr-2 { margin-left: 0.5rem; margin-right: 0; }

▶ Example: RTL-optimized dashboard layout

TSX
// components/DashboardLayout.tsx — RTL Compatible Layouts
export default function DashboardLayout({
  sidebar,
  children,
  locale
}: {
  sidebar: React.ReactNode
  children: React.ReactNode
  locale: string
}) {
  const isRtl = locale === 'ar'

  return (
    <div style={{
      display: 'flex',
      flexDirection: isRtl ? 'row-reverse' : 'row',
      minHeight: '100vh'
    }}>
      {/* Sidebar:LTR On the left,RTL On the right */}
      <aside style={{
        width: 260,
        borderRight: isRtl ? 'none' : '1px solid #e5e7eb',
        borderLeft: isRtl ? '1px solid #e5e7eb' : 'none'
      }}>
        {sidebar}
      </aside>

      {/* Main Content Area */}
      <main style={{
        flex: 1,
        padding: '2rem',
        textAlign: isRtl ? 'right' : 'left'
      }}>
        {children}
      </main>
    </div>
  )
}
⚠️ Note: RTL isn't just direction: rtl; you also need to mirror all flex directions, margins/padding, icon arrows, and text alignment.


8. Complete Example: Official Website in Four Languages + RTL Dashboard

TSX
// app/[locale]/layout.tsx — Internationalization Root Layout
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
import { routing } from '@/i18n/routing'
import { notFound } from 'next/navigation'
import LocaleSwitcher from '@/components/LocaleSwitcher'
import type { Metadata } from 'next'

type Props = { children: React.ReactNode; params: Promise<{ locale: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale } = await params
  return {
    title: 'TaskFlow',
    alternates: {
      canonical: `https://taskflow.io/${locale}`,
      languages: Object.fromEntries(
        routing.locales.map((l) => [l, `https://taskflow.io/${l}`])
      )
    }
  }
}

export default async function LocaleLayout({ children, params }: Props) {
  const { locale } = await params

  if (!routing.locales.includes(locale as any)) notFound()

  const messages = await getMessages()
  const isRtl = locale === 'ar'

  return (
    <html lang={locale} dir={isRtl ? 'rtl' : 'ltr'}>
      <head>
        {/* RTL Loading... RTL CSS */}
        {isRtl && <link rel="stylesheet" href="/rtl.css" />}
      </head>
      <body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>
        <NextIntlClientProvider messages={messages}>
          <nav style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            padding: '1rem 2rem',
            borderBottom: '1px solid #e5e7eb',
            flexDirection: isRtl ? 'row-reverse' : 'row'
          }}>
            <strong>TaskFlow</strong>
            <LocaleSwitcher />
          </nav>
          <main style={{ padding: '2rem' }}>{children}</main>
        </NextIntlClientProvider>
      </body>
    </html>
  )
}
TSX
// app/[locale]/dashboard/page.tsx — RTL Dashboard
import { getTranslations } from 'next-intl/server'

type Props = { params: Promise<{ locale: string }> }

export default async function DashboardPage({ params }: Props) {
  const { locale } = await params
  const t = await getTranslations('dashboard')
  const isRtl = locale === 'ar'

  return (
    <div>
      <h1 style={{ textAlign: isRtl ? 'right' : 'left' }}>{t('welcome', { name: 'Alice' })}</h1>

      <div style={{
        display: 'flex',
        gap: '1rem',
        flexDirection: isRtl ? 'row-reverse' : 'row'
      }}>
        <div style={{ flex: 1, padding: '1.5rem', background: '#f0f9ff', borderRadius: 8 }}>
          <h3>{t('stats.members', { count: 12 })}</h3>
        </div>
        <div style={{ flex: 1, padding: '1.5rem', background: '#fef3c7', borderRadius: 8 }}>
          <h3>{t('stats.projects', { count: 45 })}</h3>
        </div>
        <div style={{ flex: 1, padding: '1.5rem', background: '#ecfdf5', borderRadius: 8 }}>
          <h3>{t('stats.tasks', { count: 230 })}</h3>
        </div>
      </div>

      <h2 style={{ textAlign: isRtl ? 'right' : 'left' }}>{t('activity')}</h2>
      <div style={{
        padding: '1rem',
        border: '1px solid #e5e7eb',
        borderRadius: 8,
        textAlign: isRtl ? 'right' : 'left'
      }}>
        <p>Alice The project was created "Website Redesign"</p>
        <p>Bob Done Task #47</p>
        <p>Charlie Joined the organization</p>
      </div>
    </div>
  )
}
TEXT
# Output Results(Chinese):
TaskFlow
[Select a language: Chinese ▼]

Welcome back,Alice
[12 Famous members] [45 projects] [230 A task]

Recent Events
Alice The project was created "Website Redesign"
Bob Done Task #47
Charlie Joined the organization
TEXT
# Output Results(Arabic):
TaskFlow
[اختيار اللغة: العربية ▼]

مرحباً بعودتك، Alice
[12 أعضاء] [45 مشروعاً] [230 مهمة]

النشاطات الأخيرة
أليس قامت بإنشاء مشروع "إعادة تصميم الموقع"
بوب أكمل المهمة رقم 47
تشارلي انضم إلى المنظمة

❓ FAQ

Q Which should I choose, next-intl or next-international?
A next-intl has a larger community (5k⭐) and more features (plural, selection, number formatting, and RTL detection). next-international is more lightweight. This tutorial uses next-intl.
Q Why use URL rewriting instead of redirection in middleware?
A rewrite keeps the content of the browser’s address bar unchanged while internally mapping to /{locale}/path, which is better for SEO and user experience. redirect changes the URL, which may break links shared by users.
Q What additional work is required for Arabic RTL layouts?
A In addition to direction: rtl, you must mirror the flex direction (flex-direction: row-reverse), adjust the direction of margins and padding, flip arrow icons, and use text-align: start/end instead of left/right. It is recommended to handle this uniformly using the [dir="rtl"] selector in globals.css.
Q Does generateStaticParams generate a separate HTML file for each language?
A Yes. For example, /zh/blog/post-1, /en/blog/post-1, and /ar/blog/post-1 are three separate HTML files. Each file contains only the content for its corresponding language, ensuring the fastest loading speed.
Q How should variable parameters be handled in a translation dictionary?
A Use ICU MessageFormat syntax: {name} to insert variables, {count, plural, =0 {none} one {# task} other {# tasks}} to handle plurals. The t() and useTranslations() hooks in next-intl automatically parse these templates.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Install next-intl in an existing project, configure both Chinese and English dictionaries, implement a language-switching component, and use t() to render the navigation bar text on the page.

  2. Advanced Exercise (⭐⭐): Implement a complete multilingual SSG blog: generateStaticParams Pre-generate article pages for four languages; have each article load the corresponding language dictionary during the build process; and verify the structure of the generated HTML files.

  3. Challenge (⭐⭐⭐): Build a dashboard page that supports RTL: Configure it for both Arabic and English, and use the rtl: prefix and flex-direction mirroring in Tailwind CSS to implement full layout adaptation for the sidebar, navigation, and statistics cards. Verify that all elements are correctly mirrored under dir="rtl".

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏