Audit and maintain RTL (right-to-left) layout compatibility in stream-chat-react-native. Use when changing styles, positioning, flex layouts, swipe gestures, animated transforms, icons, text alignment, or anything that has a horizontal/directional axis.
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Audit and maintain RTL (right-to-left) layout compatibility in stream-chat-react-native. Use when changing styles, positioning, flex layouts, swipe gestures, animated transforms, icons, text alignment, or anything that has a horizontal/directional axis.
Use this skill whenever code changes can affect users in RTL locales (Hebrew he ships today; Arabic/Persian/Urdu integrators are common). React Native flips some layout properties automatically via I18nManager.isRTL, but absolute positioning, hardcoded margins/paddings, transforms, swipe gestures, and SVG icons must be handled by hand.
When the user asks for an "RTL audit" or "RTL review," walk the Audit checklist against the diff (or the named files), then return findings grouped by severity. When writing new code, apply the Patterns to follow rather than just the anti-patterns at the end.
Non-negotiable rules
Read direction at runtime. Use I18nManager.isRTL from react-native. Never assume LTR. Never assume a value at module load time only — I18nManager.isRTL is a static snapshot per JS bundle (RN reloads the bundle on direction change), so module-scope reads are fine, but state that depends on it must not be cached across user-driven direction toggles within a single session unless the bundle is reloaded.
Logical properties beat physical ones. Prefer start/end variants (paddingStart, marginEnd, borderStartWidth, insetStart) over left/right for spacing and borders. RN auto-flips start/end based on I18nManager.isRTL. The exception is absolute positioning — RN does NOT auto-flip left/right on absolutely positioned elements; those need an explicit I18nManager.isRTL conditional.
flexDirection: 'row' auto-flips. Default flexDirection: 'row' reverses in RTL. Do NOT counter this by manually setting 'row-reverse' for "alignment fixes" — that double-flips and breaks RTL. Only use 'row-reverse' when the visual order must be opposite of reading order in both directions.
Text alignment defaults to writing direction. For Text, default textAlign is already direction-aware. Set textAlign: 'left'/'right' ONLY when you need a fixed visual side; otherwise omit it or use textAlign: 'auto'. When you need "align to start of reading direction" explicitly, write textAlign: I18nManager.isRTL ? 'right' : 'left'.
writingDirection on Text that mixes scripts. When user-generated text could contain RTL characters (messages, channel names, member names, poll options, inputs), set writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' (iOS) so bidi resolution matches the app direction. Or wrap with WritingDirectionAwareText from package/src/components/RTLComponents/.
Mirror directional icons; don't mirror neutral ones. Arrows, chevrons, reply, send, thread, search-magnifier, message-bubble must flip in RTL. Symmetric icons (checkmark, bell, settings gear, like-heart, emoji face) must NOT flip. Use SVG transform={I18nManager.isRTL ? 'matrix(-1 0 0 1 W 0)' : undefined} where W is the SVG width.
Swipe gestures need a direction multiplier. Any gesture that moves content along the X-axis (swipe-to-reply, swipe-to-delete, paging) must multiply translationX by I18nManager.isRTL ? -1 : 1. Otherwise swipe-from-right-to-left does the wrong thing in RTL.
Backward-compatible. RTL fixes should not change LTR behavior. When in doubt, the conditional form I18nManager.isRTL ? rtl : ltr is safer than swapping a default.
Where to put what
Foundation primitives & helpers → package/src/utils/ (e.g., rtlMirrorSwitchStyle.ts) and package/src/components/RTLComponents/ (e.g., WritingDirectionAwareText.tsx).
Component-level RTL handling → in the component itself. Read I18nManager.isRTL at the top of the render or in useStyles().
Theme → there are no RTL-specific theme tokens. Don't add new directional values to theme.ts (paddingLeft, marginRight); use start/end keys instead, or compute in the consumer.
Locale files → package/src/i18n/he.json is the only shipped RTL locale. Test RTL by setting I18nManager.forceRTL(true) + reload, or by switching the device to Hebrew.
Platform divergence (iOS vs Android) → some platforms (iOS) require a transform mirror for native components like Switch. Use useRtlMirrorSwitchStyle() rather than inlining.
Keep this at component top, or compute style objects with it inside useStyles(). Don't gate behavior on Platform.OS and assume direction — RTL works on both iOS and Android.
2) Spacing: prefer logical properties
// GOOD — auto-flips
{ marginStart: 8, paddingEnd: 12, borderStartWidth: 1 }
// AVOID for spacing — does not flip
{ marginLeft: 8, paddingRight: 12, borderLeftWidth: 1 }
When migrating, the rename is direct: Left → Start, Right → End. Test once in LTR + once in RTL.
3) Absolute positioning: conditional
left / right on absolutely positioned elements do not auto-flip. Either use insetStart/insetEnd (RN 0.71+) or branch:
Common offenders: scroll-to-bottom button, online-presence dot on avatars, badge counts, overlay anchors, swipe-action content underneath a row.
4) Message-bubble alignment
Own messages render on the end side, others on the start. The alignment value ('left' | 'right') refers to physical sides for layout decisions, but for overlays/menus anchored to the bubble, flip it through:
The translate component (20 here) must equal the SVG's width so the mirror lands inside the viewBox. Special case for arrow-left.tsx: it rotates instead of matrix-mirrors — keep that style consistent with its sibling.
When adding a new icon, ask: does this icon point in a direction (e.g., →) or carry directional meaning (e.g., "next", "reply")? If yes, mirror. If no (checkmark, bell, gear, emoji), don't.
FlatListinverted works correctly in RTL (it flips along the cross axis). Horizontal FlatLists auto-reverse content order in RTL — verify visually for emoji-reaction pickers and attachment-preview strips that the start of the list is at the end of the row in LTR and at the start in RTL.
10) transform: translateX / scaleX
translateX is in absolute pixels — positive X is right on screen regardless of direction. If your animation moves "toward the end" (e.g., sliding off-screen), multiply by isRTL ? -1 : 1. scaleX: -1 is a mirror; only use it intentionally (the iOS Switch helper above, video direction in AnimatedGalleryVideo).
Anti-patterns to avoid
Hardcoded marginLeft / paddingRight for spacing — use marginStart / paddingEnd so RN can flip them. Acceptable only when you genuinely want a fixed visual side (rare).
Absolute left: X or right: X without a direction check — these do NOT flip. Add a conditional.
flexDirection: 'row-reverse' to "fix" alignment — you've broken RTL. Use 'row', which already flips correctly.
textAlign: 'left' on user content — pins text to the left even in RTL. Either omit it, use 'auto', or conditionalize on isRTL.
Setting writingDirection: 'ltr' unconditionally on user-generated text — strips bidi resolution for Arabic/Hebrew content. Branch on I18nManager.isRTL.
Mirroring symmetric icons (checkmark, bell, gear, emoji, like-heart) — they look wrong flipped. Mirror only directional icons.
Forgetting the swipe-direction multiplier on new pan gestures — the gesture activates in the wrong direction in RTL.
Caching I18nManager.isRTL at module load and assuming it never changes is fine within a session; relying on it to update mid-session without bundle reload is not — RN reloads on forceRTL change.
New directional values in theme.ts (paddingLeft, marginRight, hardcoded right: -12) — push the conditional into the consumer, or use start/end.
Assuming I18nManager.forceRTL(true) alone flips the running app — it persists for the next bundle reload. Tests must mock I18nManager.isRTL (see Testing).
Audit checklist
Walk this checklist against any diff that touches layout, positioning, gestures, transforms, icons, or text. Group findings by severity:
HIGH: visible breakage in RTL (text on wrong side, swipe wrong direction, icon points wrong way, overlay anchored to wrong edge).
MEDIUM: misaligned spacing (margins/paddings on wrong side) — readable but off.
LOW: stylistic (could use logical property but current code is technically correct).
Layout & positioning
No new marginLeft/marginRight/paddingLeft/paddingRight for spacing — use marginStart/marginEnd/paddingStart/paddingEnd.
No new borderLeftWidth/borderRightWidth/borderLeftColor/borderRightColor etc. — use borderStartWidth / borderEndWidth / borderStartColor / borderEndColor.
Any new absolute left:/right: positioning is wrapped in I18nManager.isRTL ? ... : ... (or uses insetStart/insetEnd).
No new flexDirection: 'row-reverse' introduced as an "RTL fix" (it isn't).
Negative offsets (e.g., right: -12 for an overlapping badge) are conditional on direction.
Text
No new textAlign: 'left' or 'right' on user-generated content; if needed, conditional on I18nManager.isRTL.
Text components rendering user-generated/mixed-script content set writingDirection (or use WritingDirectionAwareText).
Number-only / time / count strings are NOT given writingDirection (they're neutral).
Icons
New directional SVG icons (arrows, chevrons, send, reply, thread, message-bubble, search) have transform={I18nManager.isRTL ? 'matrix(-1 0 0 1 <width> 0)' : undefined} on the Path.
The matrix translate value matches the SVG width.
Symmetric/neutral icons (checkmark, bell, gear, like-heart, emoji) are NOT mirrored.
Gestures & animations
New Gesture.Pan() handlers that act on translationX multiply by I18nManager.isRTL ? -1 : 1.
Reanimated useAnimatedStyle returning translateX accounts for direction when "toward the end" is meant.
withSpring/withTiming targets toward an edge are flipped in RTL.
New swipe-action wrappers default side from I18nManager.isRTL if not provided.
Lists & scroll
Horizontal FlatList/ScrollView content visually starts at the end of the row in LTR (start of row in RTL) — verify or accept default RN flip.
invertedFlatList (e.g., MessageList) still renders newest at the bottom in both directions.
Native components
iOS Switch uses useRtlMirrorSwitchStyle().
TextInputtextAlign is conditional or omitted (RN handles default).
i18n
No hardcoded English/LTR-only punctuation assumptions in concatenated strings — prefer interpolation via t() with placeholders.
If adding strings, verify he.json has the same key (yarn build-translations keeps locales in sync).
Testing requirements per change
Minimum:
For visible RTL changes, manually verify in the sample app by toggling Hebrew (he) or by calling I18nManager.forceRTL(true) in index.js and reloading.
Restore between tests (afterEach(() => jest.restoreAllMocks())).
Recommended for non-trivial changes:
Render the component twice (LTR + RTL) and snapshot the resulting style props for the directional surfaces.
For gesture handlers, drive a fake Gesture.Pan with both positive and negative translationX under each direction and assert which one triggers the action.
Execution checklist (copy this when making an RTL change)
Identified directional axes in the change (spacing, absolute pos, gestures, icons, text)
Spacing uses start/end logical properties
Absolute positions are conditional on I18nManager.isRTL (or use insetStart/insetEnd)
No flexDirection: 'row-reverse' added as a flip fix
New gestures multiply translationX by direction multiplier
New directional SVG icons carry the matrix-mirror transform; symmetric ones do not
Text components with user-generated content set writingDirection
Tested with I18nManager.isRTL mocked true AND false
Visually verified in Hebrew locale (or via forceRTL(true) + reload) for non-trivial UI
yarn lint passes
yarn test:typecheck passes (run after any code change)
package/src/components/ui/Input/Input.tsx:230 and package/src/components/AutoCompleteInput/AutoCompleteInput.tsx:207 — direction-aware textAlign for inputs.
package/src/components/RTLComponents/WritingDirectionAwareText.tsx — drop-in Text with writingDirection.