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
{message.content}
``` ## 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