FrontendArabicEngineering

Building Bilingual Next.js Apps: RTL, next-intl, and Arabic Typography Done Right

A practical guide to shipping bilingual Arabic/English Next.js products with correct direction, routing, font loading, and readable Arabic typography.

BahrTech Team
May 8, 2026 · 5 min read

A client showed us their app the week before launch. The Arabic version had been "done" for a month. The dashboard looked fine in Chrome's DevTools RTL mode. In production, on real Arabic user sessions, the sidebar was pinned to the wrong side, number fields read right-to-left when they should not, and the error messages were still in English. Three days of hot-fixing before a go-live that should have been clean.

RTL and bilingual support are not a translation pass at the end of the project. They are architecture decisions that compound across every feature you ship.

Model locale and direction as first-class values

Do not derive text direction from the browser's Accept-Language header or from string content. Maintain a typed list of supported locales and a helper that maps each one to its direction explicitly.

export const locales = ["en", "ar"] as const;
export type Locale = (typeof locales)[number];

export function getDirection(locale: Locale): "ltr" | "rtl" {
  return locale === "ar" ? "rtl" : "ltr";
}

export function getDateLocale(locale: Locale): string {
  return locale === "ar" ? "ar-EG" : "en-US";
}

That helper becomes the single source of truth for the root layout, any rich text renderers, date formatters, and third-party components that need direction hints. It also makes adding a new locale — say, French for a North African market — a one-line change.

Set up next-intl as product infrastructure, not an afterthought

next-intl gives you type-safe translations, locale-aware routing, and server component support. The key is treating translation keys as stable contracts that match product language, not one-off strings scattered by feature.

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
  locales: ["ar", "en"],
  defaultLocale: "ar",
});
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { getDirection, type Locale } from "@/lib/i18n";

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: Locale }>;
}) {
  const { locale } = await params;
  const messages = await getMessages();
  return (
    <html lang={locale} dir={getDirection(locale)}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Setting dir on <html> means every browser default — scroll bars, focus rings, default padding on <input> and <select>, text alignment — flips automatically. Components that fight it are usually using hard-coded left/right offsets instead of logical CSS properties.

Tailwind logical properties are the RTL fix most teams miss

The most common RTL bug is hardcoded directional utilities: pl-4, mr-2, left-0, text-left. In an RTL layout, padding-left stays on the left side. It does not flip.

Replace directional utilities with logical ones:

ReplaceWith
pl-4, pr-4ps-4, pe-4
ml-2, mr-2ms-2, me-2
left-0, right-0start-0, end-0
text-left, text-righttext-start, text-end
border-l, border-rborder-s, border-e

This is not about Arabic specifically. It is about writing layout code that works in any text direction without per-locale overrides. If you retrofit an existing codebase, a quick grep for pl-, pr-, ml-, mr-, left-, right- in component files will surface the critical spots.

Arabic typography needs its own decisions

Arabic glyphs are more visually dense than Latin ones at the same font-size. A heading set at text-4xl in English may feel cramped and unreadable in Arabic at the same size. Common adjustments:

  • Line height: Arabic text usually needs slightly more leading- than its Latin equivalent.
  • Letter spacing: Arabic script is connected. Never use tracking-tight or tracking-widest on Arabic text — it breaks glyph joins.
  • Font choice: Load a separate Arabic-optimized font (Noto Sans Arabic, Cairo, Tajawal) and apply it only on [lang="ar"] elements.
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@400;600;700&display=swap");

:lang(ar) {
  font-family: "Noto Sans Arabic", sans-serif;
  line-height: 1.75;
  letter-spacing: 0; /* override any inherited tracking */
}

Mixed content: numbers, prices, phone numbers

Arabic-locale UIs often contain numbers and product names that stay in Latin script even when the surrounding text is RTL. Browsers handle this with the Unicode Bidi algorithm, but you can still get rendering artifacts. Use the dir="ltr" attribute on inline elements that should always read left-to-right:

<span dir="ltr">{price.toLocaleString("ar-EG")} ج.م</span>

Arabic number formatting (ar-EG) outputs Eastern Arabic numerals (٣٢٠) by default. Decide explicitly whether your users expect those or Western Arabic numerals (320), then stay consistent.

Metadata, OG images, and SEO need locale awareness

Generate <title> and <meta name="description"> from your translation messages so search engines index the correct language. Set hreflang alternates so Google knows both URLs exist:

export async function generateMetadata({ params }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "Metadata" });
  return {
    title: t("title"),
    description: t("description"),
    alternates: {
      canonical: `/${locale}`,
      languages: { ar: "/ar", en: "/en" },
    },
  };
}

Locale-aware OG images matter too. A 1200×630 image with Latin text will look off in Arabic social shares. Build OG images dynamically using Next.js ImageResponse and feed it the translated title.

Testing RTL before users do

The gap that caused the launch-week scramble above was absent RTL testing. Three habits that prevent it:

  1. Add dir="rtl" to your Storybook global decorator so component stories flip direction automatically.
  2. Include one RTL-locale test in each integration test that touches layout-sensitive UI.
  3. Test on a physical Android device set to Arabic locale, not just browser DevTools. Some flex and grid behaviors differ subtly between Chrome's emulation and real OS rendering.

A bilingual Next.js app is one product system — not two copies of the same interface with different strings. Locale, direction, typography, and metadata are first-class requirements from the first route. Get that foundation right and every feature you add afterward just works in both languages.

For the AI side of Arabic product work, see AI Chatbots That Actually Understand Arabic.

Tags
FrontendArabicEngineering