DEV Community

Cover image for Supporting 5 Languages (Including 2 RTL Ones) Almost Broke My Layout System
Talha Ramzan
Talha Ramzan

Posted on

Supporting 5 Languages (Including 2 RTL Ones) Almost Broke My Layout System

When I decided to support English, Spanish, Arabic, French, and Urdu on my tools site, I assumed the hard part would be translation. It wasn't. The hard part was that two of those five languages read right-to-left, and "just add dir="rtl"" turned out to be the beginning of the problem, not the end of it.

Here's what actually broke, and what fixed it.

The naive version (day 1)

<html dir={locale === 'ar' || locale === 'ur' ? 'rtl' : 'ltr'}>
Enter fullscreen mode Exit fullscreen mode

This works for text direction. It does not work for anything else. The moment I flipped dir to rtl, every layout built with directional assumptions, icons pointing the wrong way, padding on the wrong side, flexbox items reversing in ways I didn't want, broke simultaneously.

Problem 1: "Left" and "right" in CSS don't mean what you think

Most CSS written without RTL in mind uses physical properties: padding-left, margin-right, text-align: left. These are directionally absolute, they don't care what dir the page is in. So a "back" arrow icon with margin-right: 8px (space before the label, in LTR) ends up with the gap on the wrong side once the page flips, because the icon's logical position reversed but the CSS didn't know to follow.

The fix is switching to logical properties, which are relative to text direction instead of physical screen position:

/* Before: physical, breaks under RTL */
.back-button {
  margin-right: 8px;
  padding-left: 12px;
}

/* After: logical, follows text direction automatically */
.back-button {
  margin-inline-end: 8px;
  padding-inline-start: 12px;
}
Enter fullscreen mode Exit fullscreen mode

margin-inline-end means "the margin at the end of the reading direction", 8px on the right in LTR, 8px on the left in RTL, automatically. No conditional logic, no locale check in the component. The browser handles it based on dir.

Problem 2: Numbers and code snippets should not flip

This is the one that actually surprised me. RTL affects the reading direction of text, but numerals, currency values, and code blocks should stay LTR even inside an RTL page, nobody wants to read 123 as 321 or a JSON snippet mirrored.

<span dir="ltr" className="inline-block">
  {formattedPrice}
</span>
Enter fullscreen mode Exit fullscreen mode

Wrapping numeric/code content in an explicit dir="ltr" override, even inside an RTL parent, keeps it readable. Miss this and a WAPDA bill calculator showing "Rs. 4,500" can visually reorder the digits depending on font and browser, which is a genuinely confusing bug to explain to someone reporting it.

Problem 3: Font rendering isn't uniform across scripts

Arabic and Urdu use different script systems (Urdu is written in a Nastaliq-influenced Arabic script, but the typographic conventions differ enough that a font tuned for Arabic can render Urdu text with incorrect letter joining). Using one font stack for both was a mistake I didn't catch until a native Urdu speaker pointed out that words looked "technically correct but wrong," which is a hard bug to self-diagnose if you don't read the script.

[lang="ar"] {
  font-family: 'Noto Naskh Arabic', sans-serif;
}

[lang="ur"] {
  font-family: 'Noto Nastaliq Urdu', sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

Scoping font-family by the actual lang attribute, not just a shared "RTL font" bucket, fixed rendering quality for both without either language compromising for the other.

Problem 4: hreflang tags are easy to add and easy to add wrong

Each tool page exists in 5 locale variants, and search engines need to know they're the same content in different languages, not 5 separate, duplicate pages. That's what hreflang is for:

const languages: Record<string, string> = {};
for (const loc of LOCALES) {
  languages[loc] = toolUrl(loc, slug);
}
languages['x-default'] = canonicalUrl;
Enter fullscreen mode Exit fullscreen mode

Getting the mapping right matters more than it sounds like it should. Every locale variant needs to list every other locale variant as an alternate, including itself, a page that lists alternates for the other 4 languages but forgets to include a self-referencing hreflang tag is a common mistake that quietly confuses crawlers about which page is canonical.

The gotcha I hit: this metadata structure existed correctly from early on, but the actual visible title and description text didn't get translated, every locale was serving the same English title. Bing's crawler flagged this as "too many pages with identical titles," which is a fair complaint: the hreflang tags said "these are translations of each other," but the content proved otherwise for anyone actually reading the page.

Problem 5: Locale detection vs. locale preference are different problems

Auto-detecting a visitor's language from browser settings (navigator.language or the Accept-Language header) feels like the right default, but it fights with an explicit user choice. If someone's browser is set to Arabic but they click "English" in the site's language switcher, that choice needs to persist, otherwise every page navigation silently reverts them to the auto-detected language, which reads as broken rather than helpful.

function resolveLocale(request) {
  const explicit = getCookie('preferred-locale');
  if (explicit) return explicit;

  return detectFromAcceptLanguage(request.headers) ?? 'en';
}
Enter fullscreen mode Exit fullscreen mode

Explicit preference always wins over detection. Detection is only a first-visit default, never an override.

What I'd tell someone adding RTL support for the first time

  • Logical CSS properties aren't optional once RTL is in scope. Physical left/right properties will break silently and inconsistently, not obviously.
  • Numerals and code need explicit direction overrides, even inside RTL content, this is the bug most likely to ship unnoticed because it looks fine to anyone not reading the specific numbers carefully.
  • Don't assume one font serves multiple scripts well, even ones that look superficially related. Get someone who actually reads the language to review rendering, because typographic correctness isn't something you can proofread visually if you don't read the script.
  • hreflang tags are a promise your actual content has to keep. Metadata saying "these pages are translations of each other" doesn't make it true, the visible title/description text has to actually be translated for the promise to hold up to a crawler or a user.

The tools are live in all 5 languages if you want to see the RTL layout in action: dukotools.com/ar or dukotools.com/ur.

Happy to go deeper on the logical-properties migration in the comments if anyone's dealing with a large existing LTR-only codebase and dreading the RTL retrofit.

Top comments (0)