Skip to main content
Shipping Bilingual Next.js Apps in English and Arabic: Routing, RTL, and What Crawlers Actually See

Shipping Bilingual Next.js Apps in English and Arabic: Routing, RTL, and What Crawlers Actually See

July 22, 2026
Frontend Engineering
7 min read

At Devya we ship almost every product in English and Arabic. A bilingual Next.js site needs three things done right: one URL per language (a /en and /ar path prefix, not a cookie), a layout that flips through CSS logical properties instead of hand-patched left/right overrides, and hreflang metadata that tells search engines the two URLs are translations of each other. We learned the first one the hard way — a cookie-driven language switcher meant crawlers only ever indexed the English half of a site we built.

Key takeaways

- Serve each language on its own URL. Path prefixes such as /en/services and /ar/services are the multilingual structure Google's documentation recommends; content that switches per cookie on a single URL gets exactly one language indexed.

- Search engine crawlers do not persist cookies, and Googlebot crawls mostly from US IP addresses without varying its Accept-Language header — a cookie-based language toggle is invisible to them.

- Setting dir="rtl" on the html element plus CSS logical properties flips a layout automatically; Tailwind's ms-*, me-*, ps-*, pe-* and text-start utilities map to margin-inline-start and friends.

- Arabic fonts should load per locale through next/font — for example IBM Plex Sans Arabic with the arabic subset — so English visitors never download the Arabic font and vice versa.

- hreflang lives in generateMetadata through alternates.languages, must be reciprocal between the two locales, and needs an x-default pointing at the fallback URL.

How should you structure locale routing in the Next.js App Router?

Put the locale in the URL as the first path segment: an app/[locale] dynamic segment, so every page exists at /en/... and /ar/.... The locale layout reads the param and sets both lang and dir on the html element, and generateStaticParams prerenders both language trees.

Middleware handles exactly one job: a request to a bare path such as / or /services is redirected to its localized twin, negotiated from a saved preference cookie first and the Accept-Language header second. After that redirect, the URL — not the cookie — is the single source of truth for language. Translations themselves can be plain JSON dictionaries awaited inside server components, so no translation strings ship in the client bundle; the next-intl library earns its place when you need ICU plurals, rich formatting, and typed message keys.

Because a crawler is always a first-time visitor with an empty cookie jar: it requests the URL, receives the default language, and indexes that. The second language never enters the index. Googlebot does not persist cookies between requests, crawls predominantly from US IP addresses, and does not systematically vary its Accept-Language header — Google's own documentation recommends separate URLs per language rather than dynamic content on one URL.

Cookie switching also breaks hreflang outright, since hreflang maps languages to URLs and both languages share one. It breaks humans too: a link copied by an Arabic reader opens in whatever language the recipient's cookie says, not what the sender was looking at. And it hurts caching — a CDN must vary on the cookie to cache anything at all, while URL-prefix routing caches each language as a plain URL.

The migration costs less than teams fear: the language switcher becomes a link to the same pathname under the other prefix, and the cookie survives only to make the bare-URL redirect remember the visitor's choice.

How do we make a layout flip for RTL without rewriting the CSS?

Set dir="rtl" on the html element and write every style in CSS logical properties; the browser does the flipping. A physical property such as margin-left stays on the left in both directions, but the logical margin-inline-start means "the side where text starts" — left in English, right in Arabic. In Tailwind that is a mechanical substitution: ml-4 becomes ms-4, pr-6 becomes pe-6, text-left becomes text-start, and border-l-2 becomes border-s-2.

Flexbox and grid already follow the document direction, so most layout flips for free. What needs manual attention is a short list. Directional icons — back arrows, chevrons, next indicators — must mirror, and Tailwind's rtl:-scale-x-100 variant handles that; logos, checkmarks, and media-playback icons must not. Mixed-direction text is the subtle one: an English product name, an email address, or a code identifier inside an Arabic sentence can drag punctuation to the wrong side, and wrapping the embedded token in a bdi element (or using dir="auto" on user-generated content) isolates it. For numerals, Intl.NumberFormat decides between Western digits and Eastern Arabic digits per locale — pick one convention and apply it everywhere.

How do we load Arabic fonts without shipping them to everyone?

Declare one font per script with next/font and attach only the active locale's font variable in the layout — Inter for the Latin subset and IBM Plex Sans Arabic for the Arabic subset, both exposed under the same CSS variable. next/font self-hosts and subsets at build time, so there is no runtime request to Google Fonts and no layout shift from late-loading glyphs.

Two script-specific notes from shipping this. Arabic has no italic tradition — browsers synthesize a slant that reads as broken, so our Arabic styles never use italics and lean on weight for emphasis instead. And Arabic glyphs sit taller than Latin ones, so a line-height tuned for English headlines usually needs loosening for the Arabic rendering of the same component.

How do we wire hreflang so both languages get indexed?

Return alternates.languages from generateMetadata on every page, listing the absolute URL of each translation plus an x-default. Three rules keep it valid: the annotations must be reciprocal — the English page lists the Arabic URL and the Arabic page lists the English one, and each lists itself; the URLs must be absolute and canonical; and x-default names the page for visitors matching neither language, normally the English URL.

Localize the rest of the metadata at the same time: an Arabic page with an English title tag and meta description looks broken in Arabic search results, and answer engines quoting the page inherit the mismatch.

FAQ

Q: Does Googlebot use cookies or Accept-Language to find Arabic content?

A: No. Googlebot does not persist cookies and generally crawls without varying Accept-Language, mostly from US IPs. Content only reachable through a cookie or header switch stays unindexed — give each language its own URL.

Q: Should Arabic live on a path prefix, a subdomain, or a country TLD?

A: A path prefix such as /ar/ is the cheapest to operate: one deployment, one domain accumulating authority, and hreflang ties the variants together. Subdomains and country TLDs fit separate regional businesses, not a language variant of one site.

Q: Does dir="rtl" break flexbox or grid?

A: No. Flex rows and grid columns follow the document direction automatically. Breakage comes from physical utilities such as ml-4 or text-left and from absolutely positioned elements pinned with left or right — replace them with logical equivalents.

Q: Do we need next-intl for a bilingual site?

A: Not necessarily. An app/[locale] segment plus JSON dictionaries awaited in server components covers a static bilingual site; next-intl pays off with ICU plurals, date and number formatting, and typed message keys across a large app.

Q: Which icons should flip in RTL?

A: Only directional ones — back and forward arrows, chevrons, progress indicators. Logos, checkmarks, and media-playback controls keep their orientation.