mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
feat(ui): add RTL support for Hebrew/Arabic text in webchat (openclaw#11498) thanks @dirbalak
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test Co-authored-by: dirbalak <30323349+dirbalak@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -199,6 +199,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers: add xAI (Grok) support. (#9885) Thanks @grp06.
|
||||
- Providers: add Baidu Qianfan support. (#8868) Thanks @ide-rea.
|
||||
- Web UI: add token usage dashboard. (#10072) Thanks @Takhoffman.
|
||||
- Web UI: add RTL auto-direction support for Hebrew/Arabic text in chat composer and rendered messages. (#11498) Thanks @dirbalak.
|
||||
- Memory: native Voyage AI support. (#7078) Thanks @mcinteerj.
|
||||
- Sessions: cap sessions_history payloads to reduce context overflow. (#10000) Thanks @gut-puncture.
|
||||
- CLI: sort commands alphabetically in help output. (#8068) Thanks @deepsoumya617.
|
||||
|
||||
@@ -122,3 +122,23 @@
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
RTL (Right-to-Left) SUPPORT
|
||||
============================================= */
|
||||
|
||||
.chat-text[dir="rtl"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chat-text[dir="rtl"] :where(ul, ol) {
|
||||
padding-left: 0;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.chat-text[dir="rtl"] :where(blockquote) {
|
||||
border-left: none;
|
||||
border-right: 3px solid var(--border);
|
||||
padding-left: 0;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import type { AssistantIdentity } from "../assistant-identity.ts";
|
||||
import type { MessageGroup } from "../types/chat-types.ts";
|
||||
import { toSanitizedMarkdownHtml } from "../markdown.ts";
|
||||
import { detectTextDirection } from "../text-direction.ts";
|
||||
import { renderCopyAsMarkdownButton } from "./copy-as-markdown.ts";
|
||||
import {
|
||||
extractTextCached,
|
||||
@@ -272,7 +273,7 @@ function renderGroupedMessage(
|
||||
}
|
||||
${
|
||||
markdown
|
||||
? html`<div class="chat-text">${unsafeHTML(toSanitizedMarkdownHtml(markdown))}</div>`
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">${unsafeHTML(toSanitizedMarkdownHtml(markdown))}</div>`
|
||||
: nothing
|
||||
}
|
||||
${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}
|
||||
|
||||
24
ui/src/ui/text-direction.test.ts
Normal file
24
ui/src/ui/text-direction.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { detectTextDirection } from "./text-direction.ts";
|
||||
|
||||
describe("detectTextDirection", () => {
|
||||
it("returns ltr for null and empty input", () => {
|
||||
expect(detectTextDirection(null)).toBe("ltr");
|
||||
expect(detectTextDirection("")).toBe("ltr");
|
||||
});
|
||||
|
||||
it("detects rtl when first significant char is rtl script", () => {
|
||||
expect(detectTextDirection("שלום עולם")).toBe("rtl");
|
||||
expect(detectTextDirection("مرحبا")).toBe("rtl");
|
||||
});
|
||||
|
||||
it("detects ltr when first significant char is ltr", () => {
|
||||
expect(detectTextDirection("Hello world")).toBe("ltr");
|
||||
});
|
||||
|
||||
it("skips punctuation and markdown prefix characters before detection", () => {
|
||||
expect(detectTextDirection("**שלום")).toBe("rtl");
|
||||
expect(detectTextDirection("# مرحبا")).toBe("rtl");
|
||||
expect(detectTextDirection("- hello")).toBe("ltr");
|
||||
});
|
||||
});
|
||||
30
ui/src/ui/text-direction.ts
Normal file
30
ui/src/ui/text-direction.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* RTL (Right-to-Left) text direction detection.
|
||||
* Detects Hebrew, Arabic, Syriac, Thaana, Nko, Samaritan, Mandaic, Adlam,
|
||||
* Phoenician, and Lydian scripts using Unicode Script Properties.
|
||||
*/
|
||||
|
||||
const RTL_CHAR_REGEX =
|
||||
/\p{Script=Hebrew}|\p{Script=Arabic}|\p{Script=Syriac}|\p{Script=Thaana}|\p{Script=Nko}|\p{Script=Samaritan}|\p{Script=Mandaic}|\p{Script=Adlam}|\p{Script=Phoenician}|\p{Script=Lydian}/u;
|
||||
|
||||
/**
|
||||
* Detect text direction from the first significant character.
|
||||
* @param text - The text to check
|
||||
* @param skipPattern - Characters to skip when looking for the first significant char.
|
||||
* Defaults to whitespace and Unicode punctuation/symbols.
|
||||
*/
|
||||
export function detectTextDirection(
|
||||
text: string | null,
|
||||
skipPattern: RegExp = /[\s\p{P}\p{S}]/u,
|
||||
): "rtl" | "ltr" {
|
||||
if (!text) {
|
||||
return "ltr";
|
||||
}
|
||||
for (const char of text) {
|
||||
if (skipPattern.test(char)) {
|
||||
continue;
|
||||
}
|
||||
return RTL_CHAR_REGEX.test(char) ? "rtl" : "ltr";
|
||||
}
|
||||
return "ltr";
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../chat/grouped-render.ts";
|
||||
import { normalizeMessage, normalizeRoleForGrouping } from "../chat/message-normalizer.ts";
|
||||
import { icons } from "../icons.ts";
|
||||
import { detectTextDirection } from "../text-direction.ts";
|
||||
import { renderMarkdownSidebar } from "./markdown-sidebar.ts";
|
||||
import "../components/resizable-divider.ts";
|
||||
|
||||
@@ -375,6 +376,7 @@ export function renderChat(props: ChatProps) {
|
||||
<textarea
|
||||
${ref((el) => el && adjustTextareaHeight(el as HTMLTextAreaElement))}
|
||||
.value=${props.draft}
|
||||
dir=${detectTextDirection(props.draft)}
|
||||
?disabled=${!props.connected}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter") {
|
||||
|
||||
Reference in New Issue
Block a user