DEV Community

Idan Levi
Idan Levi

Posted on

AI Coding Agents Are Great, but They Suck at RTL. Here's How I Fixed It

RTLify CLI demo showing the init and check commands in a terminal

I build products for Hebrew-speaking users. Every time I ask an AI to generate a component, I get the same broken output — margin-left instead of margin-inline-start, ml-4 instead of ms-4, arrows pointing the wrong way, and order numbers jumping around inside RTL sentences.

I fix it. I ask for the next component. Same bugs. The AI doesn't learn — it's trained on LTR codebases and has zero awareness that RTL exists.

After months of manually fixing the same 5-6 patterns in every single component, I built RTLify — a CLI that teaches your AI editor the rules once, so you never fix them again.

npx rtlify-ai init
Enter fullscreen mode Exit fullscreen mode

One command. Works with Claude Code, Cursor, Windsurf, Cline, GitHub Copilot, Gemini CLI, and Codex CLI.


The Bugs AI Keeps Making

Let me show you exactly what goes wrong. If you've built anything in Hebrew, Arabic, Persian, or Urdu — you'll recognize every single one.

1. Physical CSS Instead of Logical

AI writes:

.sidebar {
  margin-left: 16px;
  padding-right: 8px;
  border-left: 1px solid #ccc;
  left: 0;
}
Enter fullscreen mode Exit fullscreen mode

In RTL, "left" is "right". The layout mirrors, but these properties don't. The correct version:

.sidebar {
  margin-inline-start: 16px;
  padding-inline-end: 8px;
  border-inline-start: 1px solid #ccc;
  inset-inline-start: 0;
}
Enter fullscreen mode Exit fullscreen mode

Logical properties work in both LTR and RTL. AI never uses them unless you tell it to.

2. Wrong Tailwind Classes

This is the most common one. AI writes:

<div className="ml-4 pr-6 text-left border-l-2 rounded-tl-lg">
Enter fullscreen mode Exit fullscreen mode

Every single class here is wrong for RTL. The correct version:

<div className="ms-4 pe-6 text-start border-s-2 rounded-ss-lg">
Enter fullscreen mode Exit fullscreen mode

RTLify includes a full mapping table with 20+ conversions — mlms, prpe, text-lefttext-start, float-rightfloat-end, rounded-tlrounded-ss, and more.

3. Numbers Jumping Around (Bidi Text)

This is the sneaky one. Look at this Hebrew sentence:

<p>ההזמנה שלך #12345 אושרה בהצלחה</p>
Enter fullscreen mode Exit fullscreen mode

Looks fine in your code editor. Open it in a browser with dir="rtl" — the number #12345 visually jumps to a completely wrong position. The sentence becomes unreadable.

The fix is a <bdi> tag:

<p>ההזמנה שלך <bdi>#12345</bdi> אושרה בהצלחה</p>
Enter fullscreen mode Exit fullscreen mode

Same applies to phone numbers, dates, English brand names — any LTR content inside an RTL sentence needs <bdi> wrapping. AI never does this.

4. Icons Pointing the Wrong Way

In an RTL interface, a "next" arrow should point left, not right. But AI always generates:

<ChevronRight className="w-5 h-5" />
Enter fullscreen mode Exit fullscreen mode

The fix:

<ChevronRight className="w-5 h-5 rtl:-scale-x-100" />
Enter fullscreen mode Exit fullscreen mode

Non-directional icons (home, settings, search) should NOT be flipped. AI doesn't know the difference — RTLify teaches it which ones to flip.

5. Currency and Dates

AI formats prices like this:

const price = `₪${amount}`;  // Wrong — symbol on wrong side
Enter fullscreen mode Exit fullscreen mode

The correct way:

const price = new Intl.NumberFormat('he-IL', {
  style: 'currency',
  currency: 'ILS',
}).format(amount);
// → "42.90 ₪" — symbol on correct side, RTL mark included
Enter fullscreen mode Exit fullscreen mode

Same for dates — AI defaults to MM/DD/YYYY instead of DD/MM/YYYY with Intl.DateTimeFormat.

6. React Native Gets It Even Worse

On mobile, the same problems exist but with different APIs:

// AI writes:
paddingLeft: 16
left: 0
textAlign: 'left'

// Should be:
paddingStart: 16
start: 0
writingDirection: 'rtl'
Enter fullscreen mode Exit fullscreen mode

Plus you need I18nManager.isRTL for conditional checks and transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }] for icon flipping.


How RTLify Works

There's no magic and no runtime dependency. Here's what happens when you run npx rtlify-ai init:

  1. It writes .rtlify-rules.md — a markdown file containing 8 RTL architecture rules with concrete "do this / not that" code examples. This is the full ruleset the AI will follow.

  2. It adds a 3-line pointer to your editor's config file (CLAUDE.md, .cursorrules, .windsurfrules, etc.) that tells the AI: "read .rtlify-rules.md before generating any UI code."

  3. For Claude Code users — it installs a global /rtlify command. Just type /rtlify in Claude Code and it scans, fixes, and verifies your entire project.

That's it. The AI reads the rules on every conversation. No extra prompting needed.

You can open .rtlify-rules.md and read exactly what the AI sees. Full transparency — it's just a markdown file.

The Linter

RTLify also ships with a scanner that catches violations in existing code:

npx rtlify-ai check
Enter fullscreen mode Exit fullscreen mode
  src/components/Sidebar.tsx
       3 Tailwind Physical
         Use logical classes (ms-*, me-*, ps-*, pe-*)
         <div className="ml-4 pl-6 text-left">

  src/components/OrderCard.tsx
       8 Tailwind Physical
         Use text-start / text-end
         <h2 className="text-left text-xl">פרטי הזמנה</h2>

  2 violations across 2 files
Enter fullscreen mode Exit fullscreen mode

Exits with code 1 — plug it into your CI pipeline to catch RTL violations on every PR.

The Fix Command (Cursor, Windsurf, Cline, Copilot)

Claude Code users get the /rtlify slash command. For every other editor:

npx rtlify-ai fix
Enter fullscreen mode Exit fullscreen mode

It generates a ready-to-paste prompt and copies it to your clipboard. Paste it into Cursor, Windsurf, Cline, or any AI editor — it tells the AI to scan, fix, and verify all RTL violations.


The 8 Rules

# Rule What the AI Learns
1 Logical CSS margin-inline-start not margin-left
2 Tailwind Mapping 20+ class conversions (ml-*ms-*)
3 Icon Flipping rtl:-scale-x-100 on directional icons only
4 BDI Safety <bdi> tags for numbers/English in RTL text
5 Localized Formats Intl.NumberFormat('he-IL') for currency & dates
6 Safe i18n Respects your i18n setup or lack of it
7 Complex Components Carousels, charts, sliders with dir="rtl"
8 React Native I18nManager.isRTL, paddingStart, writingDirection

Try It

npx rtlify-ai init
Enter fullscreen mode Exit fullscreen mode

Then ask your AI to "build a checkout form in Hebrew" or "create a settings screen in Arabic". You'll see the difference immediately.

GitHub: github.com/idanlevi1/rtlify
NPM: npmjs.com/package/rtlify-ai

If you're building for RTL markets — what's the most annoying RTL bug an AI has generated for you? I'd love to hear in the comments.

Top comments (6)

Collapse
 
trinhcuong-ast profile image
Kai Alder

This is super practical. The <bdi> tag issue is one I didn't even know about until I had to debug a payment confirmation page where order numbers were jumping around. Took me hours to figure out what was going on.

The approach of using a rules markdown file is clever because it works with the grain of how these AI tools already work - they all support some form of persistent context. Instead of fighting the model, you're just giving it the right reference material.

One thing I'm curious about - does the linter catch cases where someone uses dir="ltr" on individual elements as a workaround instead of proper logical properties? I've seen that pattern a lot in codebases that bolt on RTL support after the fact.

Collapse
 
idanlevi1 profile image
Idan Levi

@trinhcuong-ast That <bdi> pain on a payment page is a classic - it's the kind of silent bug that makes you question your sanity.

Great catch on the dir="ltr" workarounds. Currently, the linter doesn't flag them because distinguishing between a legitimate LTR block (like a code snippet) and a "lazy hack" requires more context than a simple regex scan.

However, the core idea of RTLify is prevention over detection. By injecting the rules into the AI's context upfront, it learns to use Logical Properties from the start, so those dir="ltr" workarounds shouldn't even be necessary in new code.

Definitely adding this to the backlog for the next linter update. Thanks for the feedback! ⭐

Collapse
 
codewithagents_de profile image
Benjamin Eckstein

Hey Idan,

First comment here — this deserves more attention.

The RTLify approach accidentally reveals something deeper: your .rtlify-rules.md isn't just a ruleset — it's a memory file for your AI. You're solving the same problem CLAUDE.md files solve for project conventions, but for a domain the AI was never properly trained on.

The key insight is "teach it once, not every session." That's not an RTL-specific problem — it's the fundamental flaw of stateless AI assistants. Without persistent context, every new conversation your AI has forgotten every correction you ever made.

The experience of correcting an AI and having it forget is exactly what we explored here — from the AI's own perspective: I Chose This Name in a Session I Can't Remember

Curious: have you considered publishing the rules file independently of the CLI? The RTL ruleset itself might be even more valuable as a standalone reference.

Collapse
 
idanlevi1 profile image
Idan Levi • Edited

Thanks @codewithagents_de , that’s a spot-on analysis!

"Teach it once, not every session" was exactly the goal. Moving from fragile prompts to a persistent Memory Layer changes the whole DX.

Regarding the standalone ruleset: I kept it inside the CLI to ensure they stay synced across different editors (Cursor, Claude, etc.) with zero friction, but the rules are open-source and ready for anyone to reference!

Appreciate the support! ⭐

Collapse
 
harsh2644 profile image
Harsh

The tag problem is the one that gets shipped to production most often because it's invisible in development. You're looking at Hebrew text, the number sits in the right place in your IDE, you run it locally everything looks fine. Then a real user opens it in a browser with proper RTL rendering and the order number is floating somewhere it shouldn't be. No error, no warning, just broken UX that's easy to miss in review.

The deeper insight here is what you've essentially built a persistent correction layer for a domain AI was never properly trained on. The model isn't going to learn RTL from your prompts. But it will follow rules if the rules are present at the start of every session. That's a fundamentally different approach than hoping the AI "gets it" eventually.

One question: have you run into cases where the rules conflict with each other for example, a component that's genuinely bidirectional and needs both LTR and RTL handling in different parts? Curious how RTLify handles that edge case.

Collapse
 
idanlevi1 profile image
Idan Levi

Spot on, @harsh2644. The issue is the sneakiest because it looks fine in the IDE but breaks in a real RTL browser.

You're right - we can't wait for the AI to "get it." We have to provide the rules upfront.

Regarding edge cases: RTLify teaches the AI to use Logical Properties (ms-4, ps-4) that adapt automatically to the nearest dir ancestor. For mixed-language dashboards, the ruleset instructs it to use per-element dir attributes and wrapping.

It covers the 95% that AI agents get wrong by default. ⭐