From 5bde4cddb2ec445c4d7e6d6f770c2213d96811c8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 28 Aug 2026 13:53:57 -0500 Subject: [PATCH] Fix RTL post alignment on native --- src/components/Post/Embed/index.tsx | 5 +++++ src/components/RichText.tsx | 10 ++++++++-- src/lib/strings/__tests__/bidi.test.ts | 20 ++++++++++++++++++- src/lib/strings/bidi.ts | 14 +++++++++++++ .../Messages/components/MessageInputEmbed.tsx | 1 + .../components/ThreadItemAnchor.tsx | 1 + .../PostThread/components/ThreadItemPost.tsx | 1 + .../components/ThreadItemTreePost.tsx | 1 + src/screens/VideoFeed/index.tsx | 4 ++++ src/view/com/post/Post.tsx | 1 + src/view/com/posts/PostFeedItem.tsx | 1 + 11 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index 5e195ac2f9..eded5f025d 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -326,6 +326,11 @@ export function QuoteEmbed({ {richText ? ( & { value: RichTextAPI | string + language?: string testID?: string numberOfLines?: number disableLinks?: boolean @@ -56,6 +58,7 @@ export type RichTextProps = TextStyleProp & export function RichText({ testID, value, + language, style, numberOfLines, disableLinks, @@ -82,7 +85,10 @@ export function RichText({ } }, [value]) - const plainStyles = style + const plainStyles = [ + style, + isRTL(language) ? native({textAlign: 'right'}) : null, + ] const suffixStyles = suffix && suffixOffset ? android({paddingBottom: suffixOffset, marginBottom: -suffixOffset}) diff --git a/src/lib/strings/__tests__/bidi.test.ts b/src/lib/strings/__tests__/bidi.test.ts index 1958aa76c7..3be7cb3b60 100644 --- a/src/lib/strings/__tests__/bidi.test.ts +++ b/src/lib/strings/__tests__/bidi.test.ts @@ -11,7 +11,7 @@ jest.mock('#/env', () => ({ }, })) -import {forceLTR} from '../bidi' +import {forceLTR, isRTL} from '../bidi' const LEFT_TO_RIGHT_EMBEDDING = '\u202A' const POP_DIRECTIONAL_FORMATTING = '\u202C' @@ -31,3 +31,21 @@ describe('forceLTR', () => { expect(forceLTR('@alice.bsky.social')).toBe('@alice.bsky.social') }) }) + +describe('isRTL', () => { + it('recognizes right-to-left languages and scripts', () => { + expect(isRTL('he')).toBe(true) + expect(isRTL('ar')).toBe(true) + expect(isRTL('az-Arab')).toBe(true) + }) + + it('recognizes left-to-right languages and scripts', () => { + expect(isRTL('en')).toBe(false) + expect(isRTL('az-Latn')).toBe(false) + }) + + it('handles missing and invalid language tags', () => { + expect(isRTL(undefined)).toBe(false) + expect(isRTL('not_a_language')).toBe(false) + }) +}) diff --git a/src/lib/strings/bidi.ts b/src/lib/strings/bidi.ts index fe312aa4fc..273336ab94 100644 --- a/src/lib/strings/bidi.ts +++ b/src/lib/strings/bidi.ts @@ -17,3 +17,17 @@ export function forceLTR(str: string) { if (IS_WEB) return str return LEFT_TO_RIGHT_EMBEDDING + str + POP_DIRECTIONAL_FORMATTING } + +/** + * Determines whether a BCP 47 language tag uses a right-to-left script. + */ +export function isRTL(language: string | undefined) { + if (!language) return false + + try { + return new Intl.Locale(language).getTextInfo().direction === 'rtl' + } catch (error) { + if (error instanceof RangeError) return false + throw error + } +} diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index d5bcd4043c..2d6642dfbd 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -239,6 +239,7 @@ function MessageInputPostEmbed({ enableTags testID="postText" value={rt} + language={record.langs?.[0]} style={[a.text_sm, t.atoms.text_contrast_high]} authorHandle={post.author.handle} numberOfLines={3} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 6a1ee4bb55..3ad7024f3a 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -405,6 +405,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ enableTags selectable value={richText} + language={record.langs?.[0]} style={[a.flex_1, a.text_lg]} authorHandle={post.author.handle} shouldProxyLinks={true} diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index 12fff63012..34ebbac187 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -327,6 +327,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({ )} @@ -1020,9 +1021,11 @@ function Overlay({ function ExpandableRichTextView({ value, + language, authorHandle, }: { value: RichTextAPI + language?: string authorHandle?: string }) { const {height: screenHeight} = useSafeAreaFrame() @@ -1057,6 +1060,7 @@ function ExpandableRichTextView({ ]}>