menu-MENAmenu-MENA
HomePricingBlogContact UsGet Started
A menu-MENA public menu page in Arabic: the restaurant name and a small teal Verified badge sit at the right side of the header, with category tabs, a search box, and item cards laid out right-to-left

Why We Didn't Just Mirror the Menu for Arabic

Menu Design

By menu-MENA Team

Published on August 29, 2026


The one-line fix that doesn't fix anything

Somewhere in most bilingual projects, someone suggests the same shortcut: add an RTL plugin, flip dir to rtl, ship it. For a fixed, self-authored interface, that shortcut mostly works. For a product where restaurant owners build their own menu layouts, upload their own PDFs, and pick their own fonts, it runs out fast. We covered the design side of Arabic-first menus in an earlier post. This one is about what had to change in the code.

Where the mirroring plugin actually lives

We do use stylis-plugin-rtl, just not where you'd expect. It's wired into our marketing site's Emotion cache: when the locale is Arabic, createEmotionCache builds a cache keyed muirtl with prefixer and rtlPlugin in the stylis pipeline; otherwise it builds a plain mui cache. That covers the site's own nav bar, footer, and blog layout, a small set of components we wrote once and control completely. Rewriting the compiled CSS after the fact is a reasonable trade there.

The public menu pages tenants serve to their guests don't load that plugin at all.

Direction as a value, not a detection

On the product side, the root layout sets dir on <html> directly from the resolved locale: dir={locale === 'ar' ? 'rtl' : 'ltr'}. Locale itself resolves in a fixed order: a ?lang= query parameter first, then a locale cookie, then the tenant's own default language. The query parameter isn't decorative. Our CDN caches by URL and ignores Set-Cookie variation, so a cookie-only language switch can serve a stale-language response to the next visitor who hits a cached URL. Direction is paired with that same locale everywhere downstream, so there's one source of truth instead of a layout guessing from the page's content.

An admin's design choice, not a guest's reading direction

The Template Builder lets an operator author a custom menu layout in either direction, stored as template.global.direction. But that's a design-time choice, not what a guest gets. Every renderer accepts a directionOverride prop, and the live public page always passes the visitor's actual locale-derived direction into it, so a template built in LTR still reads correctly for a guest browsing in Arabic, and vice versa. Inside the Template Builder itself, if the active custom template's authored direction doesn't match the restaurant's current default language, the Theme settings page shows a direction-mismatch warning instead of silently picking one. An admin should know about that conflict, not discover it from a guest's screenshot.

What a mirror plugin can't reach

Where a CSS logical property does the job, we use it and stop. A "selected" badge on a template card sits at insetInlineEnd, which the browser flips on its own when direction changes, no JavaScript involved. But a lot of the menu's layout runs through MUI's sx prop, and stylis-plugin-rtl only rewrites the compiled stylesheet it can see, not sx shorthand generated per-component. So the item-card renderer checks isRtl directly: card orientation is row-reverse instead of row when the layout puts the image beside the text, textAlign swaps between right and left, and an alignment helper remaps an authored left to right (and back) under RTL while leaving center alone, because centered content has no side to flip. None of that is one switch. It's a set of small, deliberate decisions made once in the renderer so every template built on top of it inherits them for free.

The parts that were never CSS to begin with

Some of this has nothing to do with layout. When a template's font isn't a web-safe default, we load it from Google Fonts, and if that font is in our list of Arabic-capable families (Cairo, Tajawal, Almarai, Noto Kufi Arabic, and others), the request appends &subset=arabic. An operator choosing a nice Latin display font for their build shouldn't end up silently missing Arabic glyphs because nobody asked for that character set. Prices format through Intl.NumberFormat with an ar-EG locale in Arabic and en-US in English, so digit and currency formatting follow the language instead of getting hand-flipped like a CSS property. And the PDF flipbook resolves its page-turn direction in three steps: a direction set on that specific PDF at upload, then a global flip-direction option, then a fallback to whichever language the guest is currently reading in. A flipbook turns pages with real animation physics; there's no stylesheet rule that flips that correctly, so it has to be decided as data before the component renders at all.

Add it up and none of it looks like a feature you'd put on a pricing page. It's the reason an Arabic menu on menu-MENA just works when a guest opens it: not because a plugin mirrored the page, but because direction was treated as something the code had to know, not something it could infer.

Frequently Asked Questions

No. That plugin runs on our own marketing site, wrapped around a small, fixed set of components (nav, footer, blog layout) that we control end to end. The public menu pages that restaurants serve to guests don't load it at all. Direction there is an explicit value threaded through props, not a stylesheet rewritten after the fact.

In this order: a ?lang= query parameter, then a locale cookie, then the restaurant's own default language setting. The query parameter exists specifically because our CDN caches pages by URL and ignores cookie differences, so a language switch needs to be visible in the URL or a cached response can serve the wrong language to the next visitor.

No. The direction you pick in the Template Builder is a design-time choice, useful while you're building. The live public page always renders in the visitor's own locale direction, which overrides what the template was authored in. If the two disagree, the dashboard shows a direction-mismatch warning so you know before a guest ever sees it.

It covers the flipbook too. Page-turn direction resolves in order: a direction set on that specific PDF at upload, then a global flip-direction option, then a fallback to whatever language the guest is currently browsing in. A flipbook has real page-turn animation, so this can't be handled by CSS at all; it has to be decided as data before the component ever renders.

Yes. Prices run through Intl.NumberFormat with an ar-EG locale in Arabic and en-US in English, so currency symbol placement and number formatting follow the language automatically instead of getting hand-flipped like a layout property.

It's the default behavior of the product, included at every tier. The 14-day free trial needs no credit card, and after that it's 90 EGP/month or 1,000 EGP/year for one branch, with additional branches at 50% off.