You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d624bbd
Browse filesBrowse the repository at this point in the historyBrowse files
Lay the client out right to left for RTL languages, by the common bidi guidelines
Arabic, Persian, Hebrew and the other right-to-left languages mirror the whole
interface. The direction comes with the language pack: the server's word on the
language (langPackLanguage.rtl, kept with the pack), or its code for a pack
without it. It switches live, without a reload; ?rtl=1 and I18n.forceRTL force it.
- CSS is logical; movement along the line reflects through --reflect, and the
icons that point along it flip (iconsReverse).
- Keys, swipes and drags follow the line: ArrowLeft goes on right to left,
value sliders included (WebKit steps a native range by the keys' sides, so
onRangeInlineArrowKey takes them).
- Galleries, carousels, stories and the media viewer page in reading order, and
outgoing messages sit on the left. Media playback, charts, clocks, check
marks, digits, phone numbers and URLs stay left to right.
- Text of unknown direction reads by its own (dir=auto); an English string an
RTL pack lacks is isolated left to right; the composer lays a draft out in
the direction it is sent in.
- e2e/popupSandboxRtl.spec.ts opens every popup story in both directions,
mediaViewerApp pages right to left, and AGENTS.md has the RTL section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: AGENTS.md
+93-2Lines changed: 93 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,7 +35,7 @@ pnpm lint # oxlint on the whole repo (config: .oxlintrc.json)
35
35
pnpm lint:fix # Same, with auto-fix
36
36
```
37
37
38
-
Debug query params: `?test=1` (test DCs), `?debug=1` (verbose logging), `?noSharedWorker=1` (disable shared worker), `?pfs=1` (Perfect Forward Secrecy — temporary auth keys, `src/lib/mtproto/tempAuthKeys.ts`; off by default), `?a11y=1` (the keyboard and screen-reader layer — see "Accessibility"; off by default).
38
+
Debug query params: `?test=1` (test DCs), `?debug=1` (verbose logging), `?noSharedWorker=1` (disable shared worker), `?pfs=1` (Perfect Forward Secrecy — temporary auth keys, `src/lib/mtproto/tempAuthKeys.ts`; off by default), `?a11y=1` (the keyboard and screen-reader layer — see "Accessibility"; off by default), `?rtl=1` (lays the app out right to left in whatever language is loaded — see "Right-to-left").
39
39
40
40
### Preview
41
41
@@ -107,6 +107,8 @@ Either way every `rootScope.managers` call is answered from
107
107
- The panel's theme select switches between `system` and every shipped theme
108
108
(`day` / `night` / `light` / `tinted`) and survives a reload — the mock
109
109
managers discard settings writes, so it is kept in `localStorage` instead.
110
+
The direction select beside it (`ltr` / `rtl`) is kept the same way and lays
111
+
every story out right to left; `?rtl=1` in the URL does the same.
110
112
- Going through 200+ stories takes more than one sitting, so each row has a
111
113
checkbox for "I have looked at this one" and the list can be sliced by it —
112
114
**All / Unchecked / Checked**, counted within whatever the search box is
`calc(90deg * var(--reflect))`, a shimmer or loading sweep running from the
631
+
inline start; origins use `--transform-origin-inline-start` / `-end`. Where nothing logical exists (`background-position`, `clip-path`,
632
+
shadows), override under `.is-rtl &`. Never set a logical property and its
633
+
physical twin for the same side in one block: production's lightningcss
634
+
emits the lowered logical rule after the block with a higher specificity, and
635
+
the physical one silently loses in the build only (logical radii become
636
+
`:lang()` rules — one reason `<html lang>` must be right).
637
+
- **Icons.** Glyphs that point along the line (arrows, back, send, reply,
638
+
forward, undo, list bullets) are listed in `src/iconsReverse.ts`; `Icon()` and
639
+
`<IconTsx>` give them `.icon-mirror`, which the stylesheet flips in RTL (with
640
+
`scale`, so it composes with a `transform` animation). A new directional icon
641
+
goes into that list; a glyph that points along the line in one place only (the
642
+
logo as a send plane) takes the class `icon-mirror` there — never a hand-made
643
+
`scaleX(-1)`.
644
+
- **JS geometry and input.** `getBoundingClientRect()`, `clientX`, `offsetLeft`
645
+
and `style.left` are physical: mirror whatever is measured from "the start" of
646
+
a row, and prefer writing logical styles (`style.insetInlineStart`) or a CSS
647
+
variable the stylesheet applies logically. `scrollLeft` is `0` at the start
648
+
and negative toward the end in RTL. In a horizontal widget ArrowRight means
649
+
"previous" and ArrowLeft "next" (`getInlineArrowKeys()`); horizontal swipes
650
+
and drags (swipe-to-reply, swipe-back, tab swipes) flip their sign
651
+
(`handleTabSwipe` already does).
652
+
- **Sequences page in reading order.** The media viewer, stories, the profile
653
+
photo carousel, slideshows and any other gallery or carousel put the next item
654
+
at the inline end — on the left in RTL: its arrow, the side a click goes on by,
655
+
ArrowLeft, a swipe to the right and the slide all point that way, and a
656
+
progress that counts steps (the stories strip) fills from the right. One
657
+
inside content of its own direction (a page's or a rich message's slideshow)
658
+
pages that way. Outgoing messages sit on the left.
659
+
- **Text.** Content whose direction is unknown — names, messages, captions —
660
+
gets `dir="auto"` (`setDirection()` / `setInnerHTML()`); strings that are
661
+
always left to right — phone numbers, codes, card numbers, URLs, standalone
662
+
@usernames, signed amounts, code — get `dir="ltr"`; parts joined into one line
663
+
are isolated (`unicode-bidi: isolate`) so one RTL part does not reorder its
664
+
neighbours. A string an RTL pack lacks falls back to English, which `I18n`
665
+
puts in a left-to-right isolate inside its element (`untranslatedKeys`), the
666
+
spaces that join it to its neighbours left outside. The composer lays a draft
667
+
out in the direction it is sent in (`editorDocumentRtl`, the first block with
668
+
text), not the interface's.
669
+
- **What is not mirrored.** Media playback — seek and progress bars, voice
670
+
waveforms, video timelines, play / rewind / forward — runs left to right: it
671
+
follows the tape, not the reading direction. Put the attribute `dir="ltr"` on
672
+
the control (icons inside a `[dir="ltr"]` island are not mirrored) and keep its
673
+
geometry physical. Media content, stickers, illustrations, avatars, QR codes,
674
+
maps, charts and graphs, clocks and anything that turns clockwise, check marks
675
+
and digits stay as they are. Value sliders (volume, sizes, colours) do mirror;
676
+
a native `<input type="range">` takes its arrow keys through
677
+
`onRangeInlineArrowKey` (WebKit steps a right-to-left range by the keys'
678
+
sides, not by its direction).
679
+
- **Verify it.** Look at the change in the preview with `?rtl=1` (or flip
680
+
`I18n.forceRTL()` and back) side by side with LTR: layout, open/close and
681
+
slide animations, menus and tooltips opening toward the right side, scrolling
682
+
strips, keyboard and swipe directions. For an RTL language's own text, switch
683
+
the language to Arabic or Persian in Settings — on the preview's built bundle
684
+
(`?static=1`): the dev server lays the local English strings over the
685
+
server's pack. The direction switch
686
+
itself is covered by `src/tests/langDirection.test.ts`, and
687
+
`e2e/popupSandboxRtl.spec.ts` (part of `test:popups`) opens every popup story
688
+
in both directions and fails on a box that does not stand mirrored, and checks
689
+
that a value slider runs from the right.
690
+
600
691
## What NOT to Do
601
692
602
693
(Style rules are in "Code Style"; the import-alias, `invokeApi`-from-UI, and
@@ -637,7 +728,7 @@ pnpm test:focus:app # the same, for the signed-in client (needs PLAYWRIGHT_BASE
637
728
pnpm test:a11y:app # Axe past the login screen (needs PLAYWRIGHT_BASE_URL)
638
729
pnpm test:idle:app # the profile sidebar goes idle once open (needs PLAYWRIGHT_BASE_URL)
639
730
pnpm test:peek:app # holding a photo peeks at it: chat, rich message, shared media (needs PLAYWRIGHT_BASE_URL)
640
-
pnpm test:viewer:app # the media viewer: its place and header, a step pressed mid-step, a dead picture URL, a video's stand-in (needs PLAYWRIGHT_BASE_URL)
731
+
pnpm test:viewer:app # the media viewer: its place and header, a step pressed mid-step, paging right to left, a dead picture URL, a video's stand-in (needs PLAYWRIGHT_BASE_URL)
641
732
pnpm test:calls # the calls' noise suppressor: RNNoise in its AudioWorklet, in Chromium (~30 s)
0 commit comments