9
Switch language to العربية

Internationalization

PreviousNext

Complete guide to internationalization with Arabic (RTL) and English (LTR) support

Overview

This application provides full internationalization support with Arabic (RTL) as a first-class citizen alongside English (LTR). The platform respects user preferences with intelligent locale detection and persistence.

Supported Languages

LocaleNameDirectionFlagCurrency
enEnglishLTRUSUSD
arArabicRTLSASAR

Architecture

  • URL-based routing: /[lang]/path (e.g., /en/dashboard, /ar/dashboard)
  • Locale detection: Accept-Language header with cookie persistence
  • Direction switching: dir attribute on <html> element
  • Translation system: JSON dictionaries with async loading

Quick Start

Server Components

import { getDictionary } from "@/components/local/dictionaries";
import type { Locale } from "@/components/local/config";
 
export default async function Page({ params }: { params: { lang: string } }) {
  const dictionary = await getDictionary(params.lang as Locale);
 
  return (
    <div>
      <h1>{dictionary.common.home}</h1>
      <p>{dictionary.common.loading}</p>
    </div>
  );
}

Client Components

"use client";
import { useLocale } from "@/components/local/use-locale";
 
export function LocaleIndicator() {
  const { locale, isRTL, localeConfig } = useLocale();
 
  return (
    <span className={isRTL ? "text-right" : "text-left"}>
      {localeConfig.flag} {localeConfig.nativeName}
    </span>
  );
}

Configuration

Locale Config

Location: src/components/local/config.ts

export const i18n = {
  defaultLocale: 'en',
  locales: ['en', 'ar'],
} as const;
 
export type Locale = (typeof i18n)['locales'][number];
 
export const localeConfig = {
  'en': {
    name: 'English',
    nativeName: 'English',
    dir: 'ltr',
    flag: 'US',
    dateFormat: 'MM/dd/yyyy',
    currency: 'USD',
  },
  'ar': {
    name: 'Arabic',
    nativeName: 'Arabic',
    dir: 'rtl',
    flag: 'SA',
    dateFormat: 'dd/MM/yyyy',
    currency: 'SAR',
  },
} as const;
 
export function isRTL(locale: Locale): boolean {
  return localeConfig[locale]?.dir === 'rtl';
}

Dictionary System

Loading Dictionaries

Location: src/components/local/dictionaries.ts

import "server-only";
import type { Locale } from "./config";
 
const dictionaries = {
  "en": () => import("./en.json").then((module) => module.default),
  "ar": () => import("./ar.json").then((module) => module.default),
} as const;
 
export const getDictionary = async (locale: Locale) => {
  try {
    return await (dictionaries[locale]?.() ?? dictionaries["en"]());
  } catch (error) {
    console.warn(`Failed to load dictionary for locale: ${locale}`);
    return await dictionaries["en"]();
  }
};
 
export type Dictionary = Awaited<ReturnType<typeof getDictionary>>;

Translation Files

Location: src/components/local/en.json and src/components/local/ar.json

{
  "metadata": {
    "title": "App Title",
    "description": "App Description"
  },
  "common": {
    "loading": "Loading...",
    "home": "Home",
    "language": "Language"
  },
  "navigation": {
    "docs": "Docs",
    "atoms": "Atoms"
  }
}

Type-Safe Usage

import type { Dictionary } from "@/components/local/dictionaries";
 
interface Props {
  dictionary: Dictionary;
}
 
export function Header({ dictionary }: Props) {
  return <nav>{dictionary.navigation.docs}</nav>;
}

Middleware & Routing

Locale Detection Flow

Location: src/components/local/middleware.ts

  1. Check NEXT_LOCALE cookie for user preference
  2. Parse Accept-Language header using negotiator
  3. Match against supported locales using @formatjs/intl-localematcher
  4. Redirect to localized URL if needed
  5. Set cookie for future visits (1 year expiry)
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { type NextRequest, NextResponse } from 'next/server';
import { i18n } from './config';
 
function getLocale(request: NextRequest) {
  // 1. Check cookie first
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && i18n.locales.includes(cookieLocale as any)) {
    return cookieLocale;
  }
 
  // 2. Parse Accept-Language header
  const headers = {
    'accept-language': request.headers.get('accept-language') ?? '',
  };
  const languages = new Negotiator({ headers }).languages();
 
  // 3. Match against supported locales
  return match(languages, i18n.locales, i18n.defaultLocale);
}
 
export function localizationMiddleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // Check if pathname already has a locale
  const pathnameHasLocale = i18n.locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );
 
  if (pathnameHasLocale) return NextResponse.next();
 
  // Redirect to localized URL
  const locale = getLocale(request);
  request.nextUrl.pathname = `/${locale}${pathname}`;
 
  const response = NextResponse.redirect(request.nextUrl);
  response.cookies.set('NEXT_LOCALE', locale, {
    maxAge: 365 * 24 * 60 * 60, // 1 year
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production',
  });
 
  return response;
}

Dependencies

pnpm add @formatjs/intl-localematcher negotiator
pnpm add -D @types/negotiator

Client-Side Hooks

Location: src/components/local/use-locale.ts

useLocale Hook

Returns current locale information and RTL status.

"use client";
import { useLocale } from "@/components/local/use-locale";
 
export function MyComponent() {
  const { locale, isRTL, localeConfig } = useLocale();
 
  return (
    <div dir={isRTL ? 'rtl' : 'ltr'}>
      <p>Current: {localeConfig.nativeName}</p>
      <p>Direction: {localeConfig.dir}</p>
      <p>Currency: {localeConfig.currency}</p>
    </div>
  );
}

useSwitchLocaleHref Hook

Generates URLs for switching between locales.

"use client";
import { useSwitchLocaleHref } from "@/components/local/use-locale";
import { i18n, localeConfig } from "@/components/local/config";
import Link from "next/link";
 
export function LanguageSwitcher() {
  const switchLocaleHref = useSwitchLocaleHref();
 
  return (
    <nav className="flex gap-2">
      {i18n.locales.map((locale) => (
        <Link key={locale} href={switchLocaleHref(locale)}>
          {localeConfig[locale].flag} {localeConfig[locale].nativeName}
        </Link>
      ))}
    </nav>
  );
}

RTL Support

Layout Integration

The root layout sets direction based on locale:

// src/app/[lang]/layout.tsx
import { localeConfig, type Locale } from "@/components/local/config";
 
export default async function RootLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  const { lang } = await params as { lang: Locale };
  const config = localeConfig[lang];
 
  return (
    <html lang={lang} dir={config.dir}>
      <body>{children}</body>
    </html>
  );
}

RTL Utility Classes

Location: src/styles/rtl.css

Layout Utilities

ClassDescription
rtl:flex-row-reverseReverse flex direction
rtl:space-x-reverseReverse horizontal spacing
rtl:space-y-reverseReverse vertical spacing

Margin & Padding

ClassDescription
rtl:ml-autoAuto left margin
rtl:mr-autoAuto right margin
rtl:ml-0Zero left margin
rtl:mr-0Zero right margin
rtl:pl-0Zero left padding
rtl:pr-0Zero right padding

Text Alignment

ClassDescription
rtl:text-rightAlign text right
rtl:text-leftAlign text left

Positioning

ClassDescription
rtl:left-autoAuto left position
rtl:right-autoAuto right position
rtl:left-0Left position 0
rtl:right-0Right position 0

Transform

ClassDescription
rtl:scale-x-[-1]Mirror horizontally

Border Radius

ClassDescription
rtl:rounded-l-noneNo left border radius
rtl:rounded-r-noneNo right border radius
rtl:rounded-tl-noneNo top-left radius
rtl:rounded-tr-noneNo top-right radius
rtl:rounded-bl-noneNo bottom-left radius
rtl:rounded-br-noneNo bottom-right radius

Borders

ClassDescription
rtl:border-l-0No left border
rtl:border-r-0No right border
rtl:border-lAdd left border
rtl:border-rAdd right border

Component RTL Classes

Pre-built RTL support for common components:

Cards & Navigation

ClassDescription
.card-rtlRTL card alignment
.card-header-rtlRTL card header
.card-content-rtlRTL card content
.nav-rtlRTL navigation
.nav-item-rtlRTL nav item

Forms

ClassDescription
.form-rtlRTL form alignment
.form-label-rtlRTL form labels
.form-input-rtlRTL form inputs
.form-checkbox-rtlRTL checkboxes
.btn-group-rtlRTL button groups

Layout Components

ClassDescription
.modal-rtlRTL modal
.modal-header-rtlRTL modal header
.modal-close-rtlRTL modal close button
.sidebar-rtlRTL sidebar
.sidebar-item-rtlRTL sidebar items
.dropdown-rtlRTL dropdown

Data Display

ClassDescription
.table-rtlRTL table alignment
.table-header-rtlRTL table headers
.table-cell-rtlRTL table cells
.avatar-group-rtlRTL avatar groups
ClassDescription
.breadcrumb-rtlRTL breadcrumbs
.breadcrumb-separator-rtlRTL breadcrumb separators
.pagination-rtlRTL pagination
.pagination-prev-rtlRTL previous button
.pagination-next-rtlRTL next button
.tabs-rtlRTL tabs
.tab-content-rtlRTL tab content

Interactive Components

ClassDescription
.accordion-rtlRTL accordion
.accordion-trigger-rtlRTL accordion triggers
.accordion-icon-rtlRTL accordion icons
.select-rtlRTL select
.select-trigger-rtlRTL select trigger
.select-icon-rtlRTL select icon
.command-rtlRTL command palette
.command-input-rtlRTL command input
.command-shortcut-rtlRTL command shortcuts

Date & Calendar

ClassDescription
.datepicker-rtlRTL date picker
.datepicker-header-rtlRTL datepicker header
.datepicker-nav-rtlRTL datepicker navigation
.calendar-rtlRTL calendar
.calendar-header-rtlRTL calendar header
.calendar-nav-rtlRTL calendar navigation
.calendar-grid-rtlRTL calendar grid

Other Components

ClassDescription
.toast-rtlRTL toast notifications
.toast-close-rtlRTL toast close button
.progress-rtlRTL progress bar
.sheet-rtl[data-side="right"]RTL right sheet
.sheet-rtl[data-side="left"]RTL left sheet
.scrollbar-rtlRTL scrollbar
.menu-rtlRTL menu
.menu-item-rtlRTL menu items
.menu-shortcut-rtlRTL menu shortcuts
.menu-submenu-rtlRTL submenus

RTL Animations

ClassDescription
rtl:animate-slide-in-rightSlide in from right
rtl:animate-slide-out-rightSlide out to right
rtl:hover:translate-x-2Hover translate right
rtl:hover:translate-x-[-2]Hover translate left
rtl:focus:translate-x-1Focus translate
rtl:group-hover:translate-x-2Group hover translate

CSS Logical Properties

Prefer logical properties for automatic RTL support without extra classes:

/* Instead of: */
.element {
  margin-left: 1rem;
  padding-right: 2rem;
}
 
/* Use: */
.element {
  margin-inline-start: 1rem;
  padding-inline-end: 2rem;
}

Available logical properties in src/styles/container.css:

.layout-container {
  padding-inline: var(--container-padding);
}
 
.ps-responsive {
  padding-inline-start: var(--container-padding);
}
 
.pe-responsive {
  padding-inline-end: var(--container-padding);
}
 
.ms-responsive {
  margin-inline-start: 2rem;
}
 
.me-responsive {
  margin-inline-end: 2rem;
}

Arabic Utilities

Location: src/lib/arabic-utils.ts

Number Conversion

import { toArabicNumerals, toEnglishNumerals } from "@/lib/arabic-utils";
 
// Convert to Arabic-Indic numerals
toArabicNumerals("123")     // "١٢٣"
toArabicNumerals("2024")    // "٢٠٢٤"
 
// Convert back to Western numerals
toEnglishNumerals("١٢٣")    // "123"

Number Formatting

import { formatArabicNumber, formatArabicPercentage } from "@/lib/arabic-utils";
 
// Format with Arabic thousands separators
formatArabicNumber(1234567)          // "١٬٢٣٤٬٥٦٧"
 
// Format percentage
formatArabicPercentage(75)           // "٧٥٫٠٪"
formatArabicPercentage(99.5, 2)      // "٩٩٫٥٠٪"

Currency Formatting

import { formatArabicCurrency } from "@/lib/arabic-utils";
 
formatArabicCurrency(1500)           // "١٬٥٠٠٫٠٠ ر.س"
formatArabicCurrency(99.99)          // "٩٩٫٩٩ ر.س"

Date & Time Formatting

import { formatArabicDate, formatArabicRelativeTime } from "@/lib/arabic-utils";
 
// Full date format
formatArabicDate(new Date())         // "٢٧ نوفمبر ٢٠٢٥"
 
// Relative time
formatArabicRelativeTime(pastDate)   // "منذ يومين"
formatArabicRelativeTime(futureDate) // "خلال ٣ أيام"

File Size Formatting

import { formatArabicFileSize } from "@/lib/arabic-utils";
 
formatArabicFileSize(1024)           // "١ كيلوبايت"
formatArabicFileSize(5242880)        // "٥٫٠ ميجابايت"
formatArabicFileSize(1073741824)     // "١٫٠ جيجابايت"

Arabic Pluralization

Arabic has three plural forms: singular, dual (exactly 2), and plural (3+).

import { arabicPlural } from "@/lib/arabic-utils";
 
arabicPlural(1, "طالب", "طالبان", "طلاب")  // "طالب" (singular)
arabicPlural(2, "طالب", "طالبان", "طلاب")  // "طالبان" (dual)
arabicPlural(5, "طالب", "طالبان", "طلاب")  // "طلاب" (plural)
 
// Common examples
arabicPlural(count, "يوم", "يومان", "أيام")     // day/days
arabicPlural(count, "ملف", "ملفان", "ملفات")   // file/files
arabicPlural(count, "رسالة", "رسالتان", "رسائل") // message/messages

Text Direction Detection

import { getTextDirection } from "@/lib/arabic-utils";
 
getTextDirection("Hello World")      // "ltr"
getTextDirection("مرحبا")            // "rtl"
getTextDirection("Hello مرحبا")      // "rtl" (Arabic detected)

RTL CSS Classes

import { getRTLClasses } from "@/lib/arabic-utils";
 
getRTLClasses(true)   // "rtl:text-right rtl:space-x-reverse rtl:ml-auto rtl:mr-0"
getRTLClasses(false)  // ""

Arabic Typography

import { ARABIC_TYPOGRAPHY_VARS, applyArabicTypography } from "@/lib/arabic-utils";
 
// CSS custom properties
ARABIC_TYPOGRAPHY_VARS = {
  '--font-arabic': '"Noto Sans Arabic", "Amiri", "Scheherazade New", sans-serif',
  '--font-arabic-display': '"Amiri", "Scheherazade New", serif',
  '--line-height-arabic': '1.8',
  '--letter-spacing-arabic': '0.02em'
}
 
// Apply to DOM element
applyArabicTypography(element);

Locale Check

import { isArabicLocale } from "@/lib/arabic-utils";
 
isArabicLocale("ar")     // true
isArabicLocale("ar-SA")  // true
isArabicLocale("en")     // false

Locale-Aware Formatters

Get all formatters for a locale at once:

import { getLocaleFormatters } from "@/lib/arabic-utils";
 
const formatters = getLocaleFormatters("ar");
 
formatters.formatNumber(1234)          // "١٬٢٣٤"
formatters.formatCurrency(99.99)       // "٩٩٫٩٩ ر.س"
formatters.formatPercentage(75)        // "٧٥٫٠٪"
formatters.formatDate(new Date())      // "٢٧ نوفمبر ٢٠٢٥"
formatters.formatRelativeTime(date)    // "منذ يومين"
formatters.isRTL                       // true
formatters.textDirection               // "rtl"
 
// English formatters
const enFormatters = getLocaleFormatters("en");
enFormatters.formatNumber(1234)        // "1,234"
enFormatters.isRTL                     // false

HTML Escaping

import { escapeArabicHTML } from "@/lib/arabic-utils";
 
// Safe for HTML rendering
escapeArabicHTML("<script>مرحبا</script>")
// "&lt;script&gt;مرحبا&lt;/script&gt;"

Best Practices

UI Guidelines

  • Use logical CSS properties (margin-inline, padding-inline) where possible
  • Icons and arrows must mirror for RTL when directional
  • Numbers remain Western Arabic numerals unless explicitly localized
  • Test all flows in both ar/en

Typography & Fonts

  • Use Arabic-friendly fonts (Rubik for this project)
  • Verify line-height (1.8 recommended for Arabic)
  • Test letter-spacing for Arabic script

Translation Keys

  • Keep translations collocated by feature
  • Avoid concatenating strings; prefer full-sentence translations
  • Validate translation keys at build time

Testing Checklist

  • All primary flows tested in ar/en
  • Layouts verified under RTL mirroring
  • Icons and arrows flip correctly
  • Screen readers handle language and direction
  • Dates/numbers/currency localized
  • Keyboard navigation works in mirrored layouts
  • No text overflow or truncation issues

Folder Structure

src/
  components/local/
    config.ts          # Locale config, types, isRTL helper
    dictionaries.ts    # Dictionary loader, Dictionary type
    middleware.ts      # Locale detection & redirection
    use-locale.ts      # Client hooks: useLocale, useSwitchLocaleHref
    en.json            # English translations
    ar.json            # Arabic translations
  lib/
    arabic-utils.ts    # Arabic formatting utilities (16 functions)
  styles/
    rtl.css            # RTL utility classes (80+ classes)
    container.css      # Logical property utilities

References

Internal

  • src/components/local/README.md - Detailed i18n standard documentation

External