Building RTL-Compatible Agent Interfaces: Arabic, Hebrew, and Persian Support
Implement right-to-left text support, bidirectional content handling, and UI mirroring for AI agent interfaces serving Arabic, Hebrew, and Persian-speaking users.
The RTL Challenge in AI Interfaces
Right-to-left (RTL) language support goes far beyond flipping text direction. When an AI agent serves Arabic, Hebrew, or Persian users, the entire interface layout must mirror: navigation moves to the right, progress indicators reverse, chat bubbles swap sides, and mixed-direction content (code snippets, URLs, numbers within Arabic text) must render correctly without garbling.
For AI agents specifically, the challenge intensifies because agent responses often mix RTL text with LTR elements — code blocks, technical terms, URLs, and mathematical expressions all flow left-to-right even within an Arabic response.
Detecting RTL Requirements
Determine directionality from the language code and apply it to the response context.
from dataclasses import dataclass
from typing import Set
RTL_LANGUAGES: Set[str] = {"ar", "he", "fa", "ur", "ps", "sd", "yi", "dv"}
@dataclass
class DirectionalityContext:
language: str
is_rtl: bool
base_direction: str # "rtl" or "ltr"
alignment: str # "right" or "left"
@classmethod
def from_language(cls, lang_code: str) -> "DirectionalityContext":
lang = lang_code.split("-")[0].split("_")[0].lower()
is_rtl = lang in RTL_LANGUAGES
return cls(
language=lang,
is_rtl=is_rtl,
base_direction="rtl" if is_rtl else "ltr",
alignment="right" if is_rtl else "left",
)
# Usage
ctx = DirectionalityContext.from_language("ar_SA")
print(ctx.base_direction) # "rtl"
print(ctx.alignment) # "right"
Handling Bidirectional Text in Agent Responses
Agent responses often contain embedded LTR content within RTL text. Use Unicode bidirectional control characters to prevent display corruption.
import re
# Unicode Bidi control characters
LRI = "\u2066" # Left-to-Right Isolate
RLI = "\u2067" # Right-to-Left Isolate
PDI = "\u2069" # Pop Directional Isolate
LRM = "\u200E" # Left-to-Right Mark
RLM = "\u200F" # Right-to-Left Mark
class BidiTextProcessor:
"""Process bidirectional text for correct display."""
def wrap_ltr_in_rtl(self, text: str) -> str:
"""Wrap LTR segments (code, URLs, numbers) in isolation markers within RTL text."""
# Isolate URLs
text = re.sub(
r"(https?://\S+)",
lambda m: f"{LRI}{m.group(1)}{PDI}",
text,
)
# Isolate code in single backticks
text = re.sub(
r"`([^`]+)`",
lambda m: f"`{LRI}{m.group(1)}{PDI}`",
text,
)
# Isolate standalone numbers with units
text = re.sub(
r"(\d+[\w%$]+)",
lambda m: f"{LRI}{m.group(1)}{PDI}",
text,
)
return text
def prepare_code_block(self, code: str, surrounding_dir: str) -> str:
"""Ensure code blocks always render LTR regardless of surrounding direction."""
if surrounding_dir == "rtl":
return f"{LRI}{code}{PDI}"
return code
def fix_punctuation(self, text: str, direction: str) -> str:
"""Ensure punctuation appears on the correct side for the text direction."""
if direction == "rtl":
# Arabic/Hebrew punctuation should be at the logical end
text = text.replace(f".{LRI}", f"{LRI}.")
return text
Backend Response Formatting for RTL
When the agent generates responses, annotate them with directionality metadata so the frontend can render correctly.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from typing import List
from dataclasses import dataclass, field
@dataclass
class FormattedSegment:
text: str
direction: str # "rtl", "ltr", or "auto"
segment_type: str # "text", "code", "url", "number"
@dataclass
class DirectionalResponse:
base_direction: str
segments: List[FormattedSegment] = field(default_factory=list)
class RTLResponseFormatter:
def __init__(self, bidi: BidiTextProcessor):
self.bidi = bidi
def format_response(self, text: str, lang: str) -> DirectionalResponse:
ctx = DirectionalityContext.from_language(lang)
response = DirectionalResponse(base_direction=ctx.base_direction)
# Split response into segments by code fence delimiters
fence = "~" * 3
parts = re.split(rf"({fence}\w*\n[\s\S]*?{fence})", text)
for part in parts:
if part.startswith(fence):
response.segments.append(
FormattedSegment(text=part, direction="ltr", segment_type="code")
)
elif ctx.is_rtl:
processed = self.bidi.wrap_ltr_in_rtl(part)
response.segments.append(
FormattedSegment(text=processed, direction="rtl", segment_type="text")
)
else:
response.segments.append(
FormattedSegment(text=part, direction="ltr", segment_type="text")
)
return response
UI Mirroring Metadata
Send layout hints to the frontend so the chat interface mirrors correctly for RTL users.
def generate_layout_hints(direction: str) -> dict:
"""Generate CSS/layout hints for the frontend."""
if direction == "rtl":
return {
"dir": "rtl",
"text_align": "right",
"user_bubble_side": "left", # Mirrored from LTR default
"agent_bubble_side": "right",
"input_icon_position": "left",
"scrollbar_side": "left",
"nav_direction": "row-reverse",
"font_family": "'Noto Sans Arabic', 'Segoe UI', sans-serif",
}
return {
"dir": "ltr",
"text_align": "left",
"user_bubble_side": "right",
"agent_bubble_side": "left",
"input_icon_position": "right",
"scrollbar_side": "right",
"nav_direction": "row",
"font_family": "'Inter', 'Segoe UI', sans-serif",
}
Input Handling for RTL
Agent input fields must handle mixed-direction typing. When a user types Arabic text and then inserts an English technical term, the cursor behavior and text flow must remain predictable.
class RTLInputValidator:
"""Validate and normalize RTL input before processing."""
def normalize_input(self, text: str, expected_dir: str) -> str:
"""Normalize Unicode and strip problematic bidi overrides from user input."""
import unicodedata
# Normalize to NFC form
text = unicodedata.normalize("NFC", text)
# Remove potentially malicious bidi override characters
dangerous = {"\u202A", "\u202B", "\u202C", "\u202D", "\u202E"}
for char in dangerous:
text = text.replace(char, "")
return text.strip()
def detect_mixed_direction(self, text: str) -> bool:
"""Check if text contains both RTL and LTR scripts."""
has_rtl = bool(re.search(r"[\u0600-\u06FF\u0590-\u05FF\u0750-\u077F]", text))
has_ltr = bool(re.search(r"[a-zA-Z]", text))
return has_rtl and has_ltr
FAQ
Do I need separate UI builds for RTL and LTR?
No. Modern CSS with logical properties (margin-inline-start instead of margin-left) and the dir HTML attribute handle mirroring automatically. Build one responsive interface that adapts based on the direction attribute. This is significantly easier to maintain than separate builds.
How do I handle RTL text in agent logs and debugging?
Logs should store raw Unicode text without bidi formatting characters. Add the language code and direction as structured metadata fields alongside the log entry. This keeps logs machine-readable while preserving full content. Bidi rendering should only happen at the display layer.
What fonts should I use for RTL languages?
Use the Noto font family (Google Noto Sans Arabic, Noto Sans Hebrew) as a reliable cross-platform choice. Specify RTL fonts first in your CSS font stack with LTR fonts as fallback. Ensure the font supports all diacritical marks — Arabic text without proper tashkeel rendering looks broken to native speakers.
#RTLSupport #BidirectionalText #ArabicUI #AIInterfaces #Accessibility #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.