2:I[7012,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],"MarkdownRenderer"]
4:I[9856,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],""]
5:I[4126,[],""]
7:I[9630,[],""]
8:I[4278,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"HeadingProvider"]
9:I[1476,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Header"]
a:I[3167,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Sidebar"]
b:I[7409,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"PageFrame"]
3:T2801,
# RTL Support Guide
Voice Mode v4.1 introduces comprehensive right-to-left (RTL) language support for Arabic, Urdu, and Hebrew in the chat interface.
## Overview
RTL support includes:
- **Text direction**: Automatic `dir="rtl"` for RTL content
- **Layout mirroring**: Chat bubbles, icons, and controls flip appropriately
- **Mixed content handling**: Proper rendering of RTL text with embedded LTR (numbers, English terms)
- **Input support**: RTL text input with proper cursor behavior
- **TTS integration**: RTL language detection for voice output
## Supported Languages
| Language | Code | Script | Direction |
| -------- | ---- | ----------------- | --------- |
| Arabic | ar | Arabic | RTL |
| Urdu | ur | Arabic (Nastaliq) | RTL |
| Hebrew | he | Hebrew | RTL |
| Persian | fa | Arabic | RTL |
| Pashto | ps | Arabic | RTL |
## CSS Implementation
### Base RTL Styles
```css
/* RTL container */
[dir="rtl"] {
direction: rtl;
text-align: right;
}
/* Chat message layout flip */
[dir="rtl"] .chat-message {
flex-direction: row-reverse;
}
[dir="rtl"] .chat-message.user {
margin-left: 0;
margin-right: auto;
}
[dir="rtl"] .chat-message.assistant {
margin-right: 0;
margin-left: auto;
}
/* Icon flipping */
[dir="rtl"] .icon-arrow-left {
transform: scaleX(-1);
}
[dir="rtl"] .icon-chevron-right {
transform: scaleX(-1);
}
/* Input field */
[dir="rtl"] .text-input {
text-align: right;
padding-right: 1rem;
padding-left: 2.5rem; /* Space for send button */
}
/* Scrollbar position */
[dir="rtl"] .chat-container {
direction: rtl;
}
[dir="rtl"] .chat-container::-webkit-scrollbar {
left: 0;
right: auto;
}
```
### Tailwind RTL Utilities
```css
/* RTL-aware spacing */
.rtl\:mr-4 {
margin-right: 1rem;
}
.rtl\:ml-0 {
margin-left: 0;
}
.rtl\:text-right {
text-align: right;
}
.rtl\:flex-row-reverse {
flex-direction: row-reverse;
}
```
### Usage with Tailwind
```tsx
```
## Component Implementation
### ChatMessage Component
```tsx
import { useRTL } from "@/hooks/useRTL";
interface ChatMessageProps {
message: Message;
language?: string;
}
export const ChatMessage: React.FC = ({ message, language }) => {
const { isRTL, dir } = useRTL(language || message.detectedLanguage);
return (
{message.content}
{message.sources &&
}
);
};
```
### useRTL Hook
```tsx
import { useMemo } from "react";
const RTL_LANGUAGES = new Set(["ar", "ur", "he", "fa", "ps"]);
interface RTLInfo {
isRTL: boolean;
dir: "rtl" | "ltr";
textAlign: "right" | "left";
}
export function useRTL(languageCode?: string): RTLInfo {
return useMemo(() => {
const isRTL = languageCode ? RTL_LANGUAGES.has(languageCode) : false;
return {
isRTL,
dir: isRTL ? "rtl" : "ltr",
textAlign: isRTL ? "right" : "left",
};
}, [languageCode]);
}
```
### RTLProvider Context
```tsx
import { createContext, useContext, ReactNode } from "react";
interface RTLContextValue {
isRTL: boolean;
setLanguage: (lang: string) => void;
}
const RTLContext = createContext({
isRTL: false,
setLanguage: () => {},
});
export const RTLProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [language, setLanguage] = useState("en");
const isRTL = RTL_LANGUAGES.has(language);
return (
{children}
);
};
export const useRTLContext = () => useContext(RTLContext);
```
## Mixed Content Handling
### Bidirectional Text
For messages containing both RTL and LTR content:
```tsx
// Use Unicode bidi isolation
const formatMixedContent = (text: string, isRTL: boolean) => {
// Wrap LTR content (numbers, English) in isolate marks
if (isRTL) {
return text.replace(
/(\d+|[A-Za-z]+)/g,
"\u2066$1\u2069", // Left-to-right isolate
);
}
return text;
};
// Example: "المريض عمره 45 سنة" renders correctly
{formatMixedContent(message.content, true)}
;
```
### Medical Terms in RTL
Medical terms often remain in English/Latin script:
```tsx
// Highlight medical terms while preserving RTL flow
const formatMedicalTerms = (text: string, isRTL: boolean) => {
const termPattern = /(metformin|diabetes|hypertension)/gi;
return text.split(termPattern).map((part, i) =>
termPattern.test(part) ? (
{part}
) : (
part
),
);
};
```
## Voice Mode RTL
### Language Detection
```python
from app.services.language_detector import detect_language
async def detect_and_set_direction(text: str) -> dict:
"""Detect language and determine text direction."""
detection = await detect_language(text)
is_rtl = detection.language in {"ar", "ur", "he", "fa", "ps"}
return {
"language": detection.language,
"is_rtl": is_rtl,
"direction": "rtl" if is_rtl else "ltr",
"confidence": detection.confidence
}
```
### RTL in Voice Responses
WebSocket events include RTL information:
```typescript
// Server sends direction with response
socket.on("voice:response", (event: VoiceResponseEvent) => {
// event.direction = "rtl" | "ltr"
// event.language = "ar"
setMessageDirection(event.direction);
displayMessage(event.text, event.direction);
});
```
## Input Handling
### RTL Text Input
```tsx
const VoiceInput: React.FC = () => {
const [inputDir, setInputDir] = useState<"ltr" | "rtl">("ltr");
const handleInput = (e: React.ChangeEvent) => {
const text = e.target.value;
// Detect RTL from first strong character
const firstChar = text.match(/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/);
if (firstChar) {
setInputDir("rtl");
} else if (text.match(/[A-Za-z]/)) {
setInputDir("ltr");
}
};
return (
);
};
```
### IME Support
Ensure proper Input Method Editor support for Arabic keyboards:
```tsx
// Handle composition events for Arabic input
const handleCompositionEnd = (e: React.CompositionEvent) => {
// Arabic IME composition complete
const text = e.data;
processInput(text);
};
;
```
## Accessibility
### Screen Reader Support
```tsx
{messages.map((msg) => (
))}
```
### Keyboard Navigation
```tsx
// RTL-aware keyboard navigation
const handleKeyDown = (e: KeyboardEvent) => {
if (isRTL) {
// Flip arrow key behavior for RTL
if (e.key === "ArrowLeft") {
navigateNext();
} else if (e.key === "ArrowRight") {
navigatePrev();
}
} else {
// Standard LTR behavior
if (e.key === "ArrowRight") {
navigateNext();
} else if (e.key === "ArrowLeft") {
navigatePrev();
}
}
};
```
## Testing
### Visual Regression Tests
```typescript
describe("RTL Layout", () => {
it("renders Arabic message correctly", async () => {
render();
const bubble = screen.getByRole("article");
expect(bubble).toHaveAttribute("dir", "rtl");
expect(bubble).toHaveClass("text-right");
// Visual regression check
await expect(page).toMatchSnapshot();
});
it("handles mixed RTL/LTR content", async () => {
const mixedMessage = {
content: "تناول metformin مرتين يوميا",
language: "ar",
};
render();
// Medical term should be LTR isolated
const term = screen.getByText("metformin");
expect(term).toHaveAttribute("dir", "ltr");
});
});
```
### Playwright E2E Tests
```typescript
test("Arabic conversation flow", async ({ page }) => {
await page.goto("/chat");
// Switch to Arabic
await page.click('[data-testid="language-selector"]');
await page.click('[data-testid="lang-ar"]');
// Verify RTL layout
const container = page.locator(".chat-container");
await expect(container).toHaveAttribute("dir", "rtl");
// Type in Arabic
await page.fill('[data-testid="chat-input"]', "ما هو الضغط الطبيعي؟");
await page.click('[data-testid="send-button"]');
// Verify message direction
const message = page.locator(".chat-message.user");
await expect(message).toHaveClass(/text-right/);
});
```
## Feature Flag
```typescript
// Check if RTL support is enabled
import { useFeatureFlag } from "@/hooks/useFeatureFlags";
const ChatContainer = () => {
const rtlEnabled = useFeatureFlag("ui.voice_v4_rtl_ui");
const { isRTL } = useRTL(currentLanguage);
return (
{/* Chat content */}
);
};
```
## Related Documentation
- [Voice Mode v4.1 Overview](./voice-mode-v4-overview.md)
- [Multilingual RAG Architecture](./multilingual-rag-architecture.md)
- [Lexicon Service Guide](./lexicon-service-guide.md)
6:["slug","voice/rtl-support-guide","c"]
0:["X7oMT3VrOffzp0qvbeOas",[[["",{"children":["docs",{"children":[["slug","voice/rtl-support-guide","c"],{"children":["__PAGE__?{\"slug\":[\"voice\",\"rtl-support-guide\"]}",{}]}]}]},"$undefined","$undefined",true],["",{"children":["docs",{"children":[["slug","voice/rtl-support-guide","c"],{"children":["__PAGE__",{},[["$L1",["$","div",null,{"children":[["$","div",null,{"className":"mb-6 flex items-center justify-between gap-4","children":[["$","div",null,{"children":[["$","p",null,{"className":"text-sm text-gray-500 dark:text-gray-400","children":"Docs / Raw"}],["$","h1",null,{"className":"text-3xl font-bold text-gray-900 dark:text-white","children":"RTL Support Guide"}],["$","p",null,{"className":"text-sm text-gray-600 dark:text-gray-400","children":["Sourced from"," ",["$","code",null,{"className":"font-mono text-xs","children":["docs/","voice/rtl-support-guide.md"]}]]}]]}],["$","a",null,{"href":"https://github.com/mohammednazmy/VoiceAssist/edit/main/docs/voice/rtl-support-guide.md","target":"_blank","rel":"noreferrer","className":"inline-flex items-center gap-2 rounded-md border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-sm text-gray-700 dark:text-gray-200 hover:border-primary-500 dark:hover:border-primary-400 hover:text-primary-700 dark:hover:text-primary-300","children":"Edit on GitHub"}]]}],["$","div",null,{"className":"rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6","children":["$","$L2",null,{"content":"$3"}]}],["$","div",null,{"className":"mt-6 flex flex-wrap gap-2 text-sm","children":[["$","$L4",null,{"href":"/reference/all-docs","className":"inline-flex items-center gap-1 rounded-md bg-gray-100 px-3 py-1 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700","children":"← All documentation"}],["$","$L4",null,{"href":"/","className":"inline-flex items-center gap-1 rounded-md bg-gray-100 px-3 py-1 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700","children":"Home"}]]}]]}],null],null],null]},[null,["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children","docs","children","$6","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L7",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[null,["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children","docs","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L7",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7f586cdbbaa33ff7.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","className":"h-full","children":["$","body",null,{"className":"__className_f367f3 h-full bg-white dark:bg-gray-900","children":[["$","a",null,{"href":"#main-content","className":"skip-to-content","children":"Skip to main content"}],["$","$L8",null,{"children":[["$","$L9",null,{}],["$","$La",null,{}],["$","main",null,{"id":"main-content","className":"lg:pl-64","role":"main","aria-label":"Documentation content","children":["$","$Lb",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L7",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]]}]]}]}]],null],null],["$Lc",null]]]]
c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"RTL Support Guide | Docs | VoiceAssist Docs"}],["$","meta","3",{"name":"description","content":"Guide to right-to-left language support in Voice Mode"}],["$","meta","4",{"name":"keywords","content":"VoiceAssist,documentation,medical AI,voice assistant,healthcare,HIPAA,API"}],["$","meta","5",{"name":"robots","content":"index, follow"}],["$","meta","6",{"name":"googlebot","content":"index, follow"}],["$","link","7",{"rel":"canonical","href":"https://assistdocs.asimo.io"}],["$","meta","8",{"property":"og:title","content":"VoiceAssist Documentation"}],["$","meta","9",{"property":"og:description","content":"Comprehensive documentation for VoiceAssist - Enterprise Medical AI Assistant"}],["$","meta","10",{"property":"og:url","content":"https://assistdocs.asimo.io"}],["$","meta","11",{"property":"og:site_name","content":"VoiceAssist Docs"}],["$","meta","12",{"property":"og:type","content":"website"}],["$","meta","13",{"name":"twitter:card","content":"summary"}],["$","meta","14",{"name":"twitter:title","content":"VoiceAssist Documentation"}],["$","meta","15",{"name":"twitter:description","content":"Comprehensive documentation for VoiceAssist - Enterprise Medical AI Assistant"}],["$","meta","16",{"name":"next-size-adjust"}]]
1:null