The Arabic build looks broken and the translation is fine. The sidebar sits on the left, the back arrow points left, the avatar hugs the left edge of every card: every word is Arabic, every box is where the English version left it. Text direction is an HTML attribute plus a set of CSS decisions that live in your components, and no catalog file carries it. globalize.now is AI-powered localization infrastructure that extracts the strings and keeps the catalogs in sync on every Git push.
Why does my Lovable app still read left-to-right in Arabic?
Because direction is a property of the document and the stylesheet, not of the message.
Your catalogs are pairs of source strings and translated strings. Once they compile for arand your components render it, the words change and nothing else does. Whether those catalogs are Lingui, which globalize.now writes to, or react-i18next makes no difference here. Neither format has anywhere to put "and the sidebar goes on the other side".
Two things have to change. The dir attribute on the html element tells the browser which way the inline axis runs. Your CSS has to stop naming sides and start naming the start and end of that axis. Everything else follows from those two.
What actually breaks when you add Arabic to a Lovable app?
The failures cluster in predictable places, and they are all styling rather than copy.
- Navigation drawers and sidebars stay on the physical left.
- Chevrons, back arrows and progress indicators point the wrong way.
- Asymmetric padding pins content to the wrong edge, so labels crowd one side.
text-lefton a heading keeps Arabic pinned to the left of its container.- Absolutely positioned badges and close buttons land in the opposite corner from where a reader expects them.
- Left borders that mark an active list item appear on the trailing edge instead of the leading one.
- An English product code inside an Arabic sentence reorders unpredictably without a direction hint.
None of these are translation defects. Every one of them is a physical direction baked into a class name.
How do I derive text direction from the locale tag?
Ask the locale what direction it uses, then write the answer onto the document.
Intl.Locale.prototype.getTextInfo() returns a direction of either ltr or rtl for a locale tag. It reached Baseline newly available status in July 2026, which means it landed in the current version of every core browser and not in most of the browsers your users are actually running. Treat the fallback below as the live path and the API as the thing that eventually retires it.
// getTextInfo() is not yet in TypeScript's bundled Intl.Locale definitions.
declare global {
namespace Intl {
interface Locale {
getTextInfo?(): { direction: "ltr" | "rtl" };
}
}
}
// Stopgap for runtimes without getTextInfo(). This set is the one place where
// adding a new right-to-left language still costs you a code change.
const RTL_FALLBACK = new Set([
"ar", "he", "fa", "ur", "ps", "sd", "yi", "dv", "ckb", "ug", "nqo", "syr",
]);
export function directionFor(locale: string): "ltr" | "rtl" {
let tag: Intl.Locale | undefined;
try {
tag = new Intl.Locale(locale);
} catch {
// Malformed tag. Fall through to the string path below.
}
const direction = tag?.getTextInfo?.().direction;
if (direction === "ltr" || direction === "rtl") return direction;
const language = tag?.language ?? locale.split(/[-_]/)[0].toLowerCase();
return RTL_FALLBACK.has(language) ? "rtl" : "ltr";
}The two try sites matter. new Intl.Locale() throws a RangeError on a malformed tag, and getTextInfo() is simply missing on older runtimes. Handling only the second is the usual bug: a locale like ar_EG arriving from a query string sends the function into its own recovery path and straight back out again as an uncaught error.
Set both attributes wherever your app already knows the active locale. Lovable currently scaffolds two shapes, a Vite single-page app and a server-rendered TanStack Start app, so check what your project actually generated. On TanStack Start it is the root document component. On Vite it is wherever you mount the i18n provider.
<html lang={locale} dir={directionFor(locale)}>Put dir on html rather than on a wrapper div. Portals, modals and toasts render outside your component tree and still need to inherit it.
Which Tailwind classes do I have to change for right-to-left?
The ones that name a physical side. Their logical equivalents follow the inline axis and flip themselves when dir changes.
| Physical utility | Logical replacement | What it sets |
|---|---|---|
| ml-4 / mr-4 | ms-4 / me-4 | margin-inline-start / margin-inline-end |
| pl-6 / pr-6 | ps-6 / pe-6 | padding-inline-start / padding-inline-end |
| left-0 / right-0 | start-0 / end-0 | inset-inline-start / inset-inline-end |
| text-left / text-right | text-start / text-end | inline-axis text alignment |
| border-l / border-r | border-s / border-e | leading / trailing border |
| rounded-l-lg / rounded-r-lg | rounded-s-lg / rounded-e-lg | leading / trailing corners |
| float-left / float-right | float-start / float-end | float on the inline axis |
| clear-left / clear-right | clear-start / clear-end | clear on the inline axis |
| scroll-ml-4 / scroll-mr-4 | scroll-ms-4 / scroll-me-4 | scroll-snap margin |
Tailwind's margin documentation shows the effect directly: ms-8 and me-8 rendered inside dir="ltr" and dir="rtl" containers sit on opposite sides. Logical property utilities arrived in Tailwind v3.3, so a long-lived project may not have them.
Two utilities need a second look rather than a rename. space-x-* and divide-x-* add spacing and borders between siblings, so in a reversed row they land on the wrong side of each child. Tailwind ships space-x-reverse and divide-x-reverse for exactly that case. Vertical utilities such as mt-* and pb-* need nothing, since direction only governs the inline axis.
What can logical properties not fix?
Anything that encodes a direction outside the box model, which is where the rtl: and ltr: variants earn their place.
<ChevronRight className="rtl:rotate-180" />
<div className="bg-[url(/hero.svg)] bg-left rtl:bg-right" />
<div className="shadow-[4px_0_8px_rgba(0,0,0,.15)] rtl:shadow-[-4px_0_8px_rgba(0,0,0,.15)]" />Transforms, background positions, shadow offsets, gradient angles, carousel scroll maths and any icon that means "forward" belong here. Keep the list short. Writing rtl: on every second element means the underlying styles are still physical and should be converted instead.
User-generated content needs its own handling, and the two cases are different. For a whole block whose language you do not control, dir="auto"lets the browser set the block's base direction from its first strong character.
<p dir="auto">{review.body}</p>For a foreign-script run inside an already-directed sentence, an English SKU sitting in Arabic body text, dir="auto"does nothing, because it sets the paragraph's base direction rather than isolating an inline run. Wrap that run instead.
<bdi>{product.sku}</bdi>Why can't a translation widget flip your layout for you?
Because a widget operates on text nodes in the rendered page, and your direction problem lives in the styles that produced that page.
A runtime service can inject dir="rtl" onto the document and translate visible strings after the English version has painted. It cannot open your components and turn ml-4 into ms-4, because ml-4 has already compiled to margin-left in a stylesheet the service does not own. What you get is Arabic text inside left-to-right furniture.
There is a second cost. Lovalingo, a runtime translation service used on some Lovable apps, has announced that it closes on 31 August 2026 at 23:59 CEST. Its migration guidance tells customers to move to project-owned internationalization and to re-verify locale routes, canonical URLs and hreflang before removing it. Layout work done inside a vendor's product leaves with the vendor. Logical utilities and a dir attribute in your own repository do not. Our Lovalingo comparison makes the same argument about the string layer.
How do you stop the next Lovable prompt from undoing it?
Convert once, then make the physical utilities visible in review.
- Install the workflow. In the Lovable workspace, add the
lovable-i18nskill and prompt Lovable to set up i18n. Outside the editor the install isnpx skills add globalize-now/globalize-skills, with--allto install for every agent. - Set direction from the locale using
directionForabove, on thehtmlelement. - Convert the physical utilities to logical ones across your components, starting with whatever wraps your pages.
- Add
rtl:variants only for transforms, backgrounds, shadows and directional icons, and reach for<bdi>on inline foreign-script runs. - Add a grep gate to review so a newly generated physical utility fails before it merges.
rg -n '\b(ml|mr|pl|pr|scroll-m[lr])-|\b(float|clear|text)-(left|right)\b|\bborder-[lr]\b|\brounded-[lr]-|\b(left|right)-[0-9]' src/Step five is the one that holds. Lovable regenerates components on every prompt, and a model asked to add a settings panel will reach for ml-4, because physical utilities dominate the React code these models were trained on. A check that fails the pull request is cheaper than finding the problem in an Arabic screenshot three weeks later.
The string half of the loop runs on its own. globalize.now extracts new hardcoded strings from whatever Lovable generated, writes them into your catalogs, and syncs on every Git push, so the Arabic build stays current while you keep prompting. That is the same mechanism described in the Vite guide and the TanStack Start guide, and it is why translations stop breaking on every push.
Adding Arabic costs nothing extra on the plan. The Starter plan is €20 per month per workspace, with €20 of translation credit (about 200,000 words) included and usage continuing at the same per-character rate after that, no per-seat and no per-language charges, and right-to-left languages included. There is more on the setup at the Lovable integration page, engineering detail on the developers page, and a shorter path for non-technical builders on the vibe coders page.
One layout change, then it stays fixed
Direction is a decision your repository makes once. The strings are the part that changes every time you prompt, and that part runs itself.
globalize.now turns hardcoded app copy into translation-ready locale files and keeps them updated as you ship.
Try globalize.now free