Lesson 24 / 25
Right-to-Left Languages
The style layer's counterpart to a direction change, logical properties taking precedence over physical ones, icon mirroring tied to a criterion, isolation in bidirectional text, and keyboard contracts written by flow direction.
Contents
Every example in the previous lesson used languages written left to right. When the catalog opens into a right-to-left language, what changes is not just the text’s direction: a record card’s border moves to the opposite side, the filter panel relocates to the other edge of the screen, a forward arrow flips, and a clock icon stays exactly as it was.
This lesson treats the direction change not as a translation problem but as an architectural decision. The decision is made in the style layer, and its scale is how many declarations are affected by the direction change.
Logical, Not Physical, Axes
Left and right are the screen’s fixed directions. Start and end depend on flow: in a left-to-right layout, start is on the left; in a right-to-left layout, it is on the right. A component whose border is written as “left” stays on the left even after the direction flips, and the layout breaks; the same border written as “inline start” follows the flow.
The computation below lists declarations gathered from catalog components alongside their physical and logical counterparts, and counts how many are direction-sensitive.
// direction.mjs — direction-sensitive layout: logical properties, mirrored and unmirrored elements // 1. Physical properties' logical counterparts. const MAPPING = { "margin-left": "margin-inline-start", "margin-right": "margin-inline-end", "padding-left": "padding-inline-start", "padding-right": "padding-inline-end", "border-left": "border-inline-start", "text-align: left": "text-align: start", "left": "inset-inline-start", "width": "inline-size", "margin-top": "margin-block-start", "border-bottom": "border-block-end", }; // Declarations gathered from catalog components. const DECLARATIONS = [ { component: "record-card", declaration: "padding-left" }, { component: "record-card", declaration: "border-left" }, { component: "record-card", declaration: "margin-top" }, { component: "filter-panel", declaration: "width" }, { component: "filter-panel", declaration: "text-align: left" }, { component: "borrow-button", declaration: "margin-right" }, { component: "notification-banner", declaration: "left" }, { component: "notification-banner", declaration: "border-bottom" }, ]; const horizontal = (d) => /left|right|width|inline/.test(d); console.log("component physical declaration logical counterpart direction-sensitive"); let sensitive = 0; for (const d of DECLARATIONS) { if (horizontal(d.declaration)) sensitive++; console.log( `${d.component.padEnd(21)} ${d.declaration.padEnd(21)} ${MAPPING[d.declaration].padEnd(25)} ${horizontal(d.declaration) ? "yes" : "no"}` ); } console.log(`direction-sensitive declarations: ${sensitive} / ${DECLARATIONS.length}`); console.log("declarations on the vertical axis are unaffected by direction change; writing"); console.log("them logically still pays off the same way in vertical writing modes."); // 2. Mirrored and unmirrored elements. const ELEMENTS = [ { name: "back arrow", mirrors: true, reason: "direction follows the flow" }, { name: "forward arrow", mirrors: true, reason: "direction follows the flow" }, { name: "breadcrumb separator", mirrors: true, reason: "shows hierarchy direction" }, { name: "progress bar", mirrors: true, reason: "increase direction follows the flow" }, { name: "clock icon", mirrors: false, reason: "direction is fixed in the real world" }, { name: "checkmark", mirrors: false, reason: "direction carries no meaning" }, { name: "search magnifier", mirrors: false, reason: "the object's own shape" }, { name: "volume icon", mirrors: true, reason: "increase direction follows the flow" }, { name: "pencil/edit", mirrors: false, reason: "the object's own shape" }, ]; console.log("\nelement mirrors reason"); for (const e of ELEMENTS) console.log(` ${e.name.padEnd(21)} ${(e.mirrors ? "yes" : "no").padEnd(8)} ${e.reason}`); console.log(`mirrored: ${ELEMENTS.filter((e) => e.mirrors).length} / ${ELEMENTS.length}`); // 3. Numbers and embedded Latin terms keep their own direction. // Bidirectional text: the paragraph's base direction is right-to-left, the number inside it is left-to-right. const SEGMENTS = [ { text: "shelf code", direction: "rtl" }, { text: "QB-141", direction: "ltr" }, { text: "year", direction: "rtl" }, { text: "1998", direction: "ltr" }, ]; console.log("\nparagraph base direction: rtl"); console.log("segment direction isolation needed"); for (const s of SEGMENTS) { console.log(` ${s.text.padEnd(11)} ${s.direction.padEnd(10)} ${s.direction === "ltr" ? "yes" : "no"}`); } console.log("without isolation, the punctuation next to a left-to-right segment drifts to"); console.log("the wrong side; the embedded segment is isolated for this reason."); // 4. A string's logical order versus its visual order: isolated and not. // Simple model: in an rtl paragraph, segments reverse order; an ltr segment stays flat inside itself. function visualOrder(segments, base) { const list = base === "rtl" ? [...segments].reverse() : segments; return list.map((s) => s.text).join(" "); } console.log("\nlogical order : " + SEGMENTS.map((s) => s.text).join(" ")); console.log("visual order : " + visualOrder(SEGMENTS, "rtl")); // 5. Scrolling and the keyboard: arrow keys keep their physical meaning. const KEYS = [ { key: "Right arrow", ltr: "next item", rtl: "previous item" }, { key: "Left arrow", ltr: "previous item", rtl: "next item" }, { key: "Home", ltr: "first item", rtl: "first item" }, { key: "End", ltr: "last item", rtl: "last item" }, ]; console.log("\nkey left-to-right right-to-left"); for (const k of KEYS) console.log(` ${k.key.padEnd(13)} ${k.ltr.padEnd(19)} ${k.rtl}`); console.log("arrow keys follow physical direction: the right arrow always moves right"); console.log("on screen, so in a right-to-left layout it reaches the previous item."); // 6. Audit: components with a remaining physical declaration cannot ship. const remaining = DECLARATIONS.filter((d) => horizontal(d.declaration)); const components = [...new Set(remaining.map((d) => d.component))]; console.log(`\ncomponents with a remaining physical declaration: ${components.length}`); for (const c of components) { const count = remaining.filter((x) => x.component === c).length; console.log(` ${c.padEnd(21)} ${count} declaration${count === 1 ? "" : "s"}`); }
component physical declaration logical counterpart direction-sensitive record-card padding-left padding-inline-start yes record-card border-left border-inline-start yes record-card margin-top margin-block-start no filter-panel width inline-size yes filter-panel text-align: left text-align: start yes borrow-button margin-right margin-inline-end yes notification-banner left inset-inline-start yes notification-banner border-bottom border-block-end no direction-sensitive declarations: 6 / 8 declarations on the vertical axis are unaffected by direction change; writing them logically still pays off the same way in vertical writing modes. element mirrors reason back arrow yes direction follows the flow forward arrow yes direction follows the flow breadcrumb separator yes shows hierarchy direction progress bar yes increase direction follows the flow clock icon no direction is fixed in the real world checkmark no direction carries no meaning search magnifier no the object's own shape volume icon yes increase direction follows the flow pencil/edit no the object's own shape mirrored: 5 / 9 paragraph base direction: rtl segment direction isolation needed shelf code rtl no QB-141 ltr yes year rtl no 1998 ltr yes without isolation, the punctuation next to a left-to-right segment drifts to the wrong side; the embedded segment is isolated for this reason. logical order : shelf code QB-141 year 1998 visual order : 1998 year QB-141 shelf code key left-to-right right-to-left Right arrow next item previous item Left arrow previous item next item Home first item first item End last item last item arrow keys follow physical direction: the right arrow always moves right on screen, so in a right-to-left layout it reaches the previous item. components with a remaining physical declaration: 4 record-card 2 declarations filter-panel 2 declarations borrow-button 1 declaration notification-banner 1 declaration
The first section’s output shows that six of eight declarations are direction-sensitive. The two declarations on the vertical axis are unaffected by the direction change, but writing them logically still pays off the same way in vertical writing modes. The rule is simple: every declaration that touches the horizontal axis is written logically, and this is a decision made before translation begins.
The last section turns the same data into an audit finding: four components still carry a physical declaration. This is the work item that has to close before a language ships, and it is automated the same way as the audits in the Tokens topic.
Not Every Icon Mirrors
The second section separates the icons. The mirroring criterion is a single question: does the icon’s direction depend on flow, or is it the object’s own shape? A forward arrow depends on flow and mirrors; a breadcrumb separator shows hierarchy direction and mirrors; a progress bar takes its increase direction from the flow and mirrors.
A clock icon does not mirror, because a clock’s direction is fixed in the real world. A checkmark does not mirror, because its direction carries no meaning. A magnifier and a pencil do not mirror, because they are the object’s own shape. Five of nine icons mirror; the distinction is made by what the icon represents, not by a blanket transform. A rule that mirrors every icon flips the clock.
Numbers and Embedded Terms Keep Their Own Direction
The third and fourth sections take on bidirectional text. Inside a right-to-left paragraph, numbers and Latin-script terms — a shelf code, an ISBN, a unit of measure — are written in their own direction. When the paragraph’s base direction and a segment’s direction differ, which side the surrounding punctuation falls on becomes ambiguous.
The fix is to isolate the embedded segment: it declares its own direction and does not affect its neighbors’. The fourth section’s comparison shows that logical order and visual order are not the same — the text’s order in memory differs from its order on screen, and that difference also shows up in copying, search, and sorting.
The Keyboard Follows Physical Direction
The fifth section writes down a rule that is often overlooked. Arrow keys follow physical direction: the right arrow always moves right on screen. In a right-to-left list, moving right on screen means reaching the list’s previous item.
This means the keyboard contracts from the Tabs and Menus lessons have to be rewritten for right-to-left layout: “the right arrow moves to the next item” is a direction-bound sentence. The correct wording is flow-based — “the arrow in the flow direction moves to the next item.” Home and End are unaffected by the direction change, because the first and last items are defined by the flow.
Measurable Constraints
- Every declaration that touches the horizontal axis is written with logical properties; the audit reports components with a remaining physical declaration.
- Icon mirroring is decided one at a time; mirroring everything breaks a clock, a checkmark, and shape-based icons.
- Embedded reversed-direction segments are isolated; unisolated concatenation leaves punctuation position ambiguous.
- Arrow-key contracts are written by flow direction rather than physical direction.
- The text-growth rule from the previous lesson still applies in right-to-left languages; the two constraints are audited together.
Common Mistakes
The most common mistake is treating direction support as a translation task and never touching the style layer — the text gets translated but the layout stays left to right, and the interface becomes unreadable. The second is mirroring every icon in bulk. The third is declaring the direction change with a single root attribute and leaving components’ physical declarations untouched: a root declaration alone is not enough without fixing declarations at the component level.
Summary
- Left and right are the screen’s fixed directions; start and end depend on flow. Every declaration on the horizontal axis is written logically — six of the eight declarations in the model are direction-sensitive.
- Icon mirroring is tied to a criterion: an icon whose direction follows the flow mirrors, one that is the object’s own shape does not; five of nine icons mirror.
- In a right-to-left paragraph, numbers and Latin-script terms keep their own direction and are isolated; the text’s logical order and its visual order are not the same.
- Arrow keys follow physical direction, so keyboard contracts are written by flow direction.
- The audit lists components carrying a physical declaration; this list closes before a language ships.
Next Step
Direction and formatting decisions adapted the interface’s container to the language. What is left is the data that goes inside the container. Every form that asks a user for a name, an address, or an identity number carries an assumption: that a name has two parts, that an address is completed by a postal code, that an identity number has a fixed length. Every one of these assumptions breaks somewhere. The course’s last lesson takes on these fields and turns the assumptions into rules.
To keep your progress and take notes, Log in
My notes
Log in to take notes.