
Why We Didn't Just Mirror the Menu for Arabic
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.