Skip to content
Back to skills

Rtl And Direction

ASecurity

Makes Expo apps right-to-left correct - logical styles, guarded direction restart, mirroring, digits, bidi. Use when adding Arabic or Hebrew, a row, icon, number, language switch or RTL test. Not for translation (i18n-strings-and-catalogs).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentrustgosqlreactnodeawstestinggitdatabase

Works with

  • cli

Security analysis

A100/100

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill rtl-and-direction --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rtl And Direction?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Rtl And Direction
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-rtl-and-direction/badge)](https://www.skillsdirectory.com/skills/zakariaf-rtl-and-direction)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: rtl-and-direction
description: Makes Expo apps right-to-left correct - logical styles, guarded direction restart, mirroring, digits, bidi. Use when adding Arabic or Hebrew, a row, icon, number, language switch or RTL test. Not for translation (i18n-strings-and-catalogs).
---

# Right-to-left layout and direction

Makes an Expo app correct in right-to-left languages on iOS and Android: screens mirror by
themselves through logical styles, the layout direction is decided before the app starts and
changes with exactly one guarded restart (never a loop), text reads in its own direction, physical
surfaces and non-directional icons stay put, numbers show the language's digits, and a
zero-dependency checker proves the code keeps it that way.

## Rules that must hold

1. **Style with logical keys and build rows with `flexDirection: 'row'`.** `marginStart/End`,
   `paddingStart/End` or `paddingInline`, `start`/`end`, `border*Start*/*End*` radii and widths;
   never physical left/right keys in a style (Reanimated styles and typed style helpers included),
   never `row-reverse`, never `scaleX: -1` on content. Why: React Native 0.86 does swap physical
   keys to start/end in a right-to-left layout, but only while a global, persisted native
   preference (`swapLeftAndRightInRTL`) is on, which any library can turn off; next to a logical
   key on the same element the swapped key wins in RTL only; react-native-web never swaps. A
   truly physical position (a drag thumb) is `transform: [{ translateX }]`, never `left`.
   (`physical-style-key`, `row-reverse`, `scale-x-flip`)
2. **Render text through `AppText` and align with `align="start" | "end"`.** Only the design
   system's text style hook maps start/end to `textAlign` and sets `writingDirection`; only AppText
   imports `Text`. Why: `textAlign: 'auto'` follows a text's first letter, not the layout, and
   React Native swaps `'left'`/`'right'` in a right-to-left layout, so `'right'` lands on the end
   (left) edge (verified on iOS).
   (`text-align-literal`, `writing-direction`, `text-import`)
3. **Keep one layout-direction source.** Only `src/i18n/direction.ts` touches `I18nManager`; layout
   decisions (icon flips, coordinate maps, navigators) read `useDirection()` or
   `readLayoutDirection()`, never `directionOf(language)`; text takes its language's direction
   through `TextLocaleProvider`. Why: `isRTL` is fixed for the whole JS run, so between a language
   change and the restart the language and the layout differ. (`i18nmanager`, `layout-direction`)
4. **Decide the direction before the app starts.** `DirectionGate` is the root layout's first,
   outermost element (only effect-free wrappers such as `GestureHandlerRootView` around it); the
   startup work with effects (database open, migrations, stores, providers) lives in its child. Why: a restart re-runs every module and startup step, so work done before the
   decision happens twice. (`direction-before-render`)
5. **Change direction only through `restartForDirection()`.** It records the pending direction in
   the guard first, sets `allowRTL` and `forceRTL` together, awaits the optional `beforeReload`
   hook (an app with audio passes `() => services.audio.dispose()`, to `DirectionGate`'s
   `beforeReload` prop too), and reloads once; call it from a
   mounted component after the language is saved. A flip that does not take becomes a reported
   give-up. Why: reloading during bundle evaluation crashed a Release build, and without the guard a
   failed flip restarts forever. (`direction-module`, `reload-outside-direction`,
   `reload-at-module-scope`)
6. **Give `expo-localization` `supportedLocales` only.** Never `supportsRTL` or `forcesRTL` (nor in
   `extra`). Why: the native module re-applies them from the device language at every launch and
   undoes an in-app choice (read in expo-localization 57.0.2's source). An app with no language
   choice of its own may opt into the `directionSource: "device"` profile in `contracts/rtl.json`:
   no restart machinery and no RTL flag needed (`supportsRTL` is tolerated but makes iOS follow the
   device language too), `forcesRTL` still banned. (`expo-localization-rtl`)
7. **A root drawn outside the app's providers sets its own text locale.** The restart screen, the
   root `ErrorBoundary` (its fallback component, or the JSX it draws inline) and any other outside
   root render `<TextLocaleProvider value={{ language, direction: directionOf(language) }}>`, with
   the language their text is in (the crash fallback: `languageNow()`, the source of its copy). Why:
   without it AppText falls back to English, left to right, and Arabic text is laid out backwards
   in the Latin line height. (`root-direction-provider`)
8. **Physical surfaces stay left to right.** Maps, media scrubbers, drawing pads and boards built
   from views sit in `PhysicalDirectionView` (list them in `contracts/rtl.json`); content that reads
   in the language's direction opts in with `mirrorsInRtl`. Gestures and safe-area insets stay
   physical: take insets symmetrically, never as left/right style keys. Why: north does not move to the right in Arabic, and media time runs left to right.
   (`physical-surface-ltr`)
9. **Flip only icons that point along the reading direction, inside `Icon`.** `DIRECTIONAL_ICONS`
   equals the directional names (`back`, `forward`, `chevron`, `undo`, `redo` by default) that
   exist in the icon set; play, clocks, checks and pictures never flip. Why: a call site always
   forgets, and a flipped play button is wrong. (`directional-icons`)
10. **Show every number in the language's digits.** Format with the tag from
    `localeTagFor(language, digits)` (`createNumberFormatter`, `{n, number}` messages); never
    `String(n)`, `.toString()` or `.toFixed()` for display; storage keeps plain numbers. Why: Hermes
    ignores the numbering system without the polyfill and the tag, and CLDR defaults move.
    (`number-to-text`; i18n-strings-and-catalogs checks `Intl.NumberFormat` itself)
11. **Isolate free text, never hand-write bidi controls.** `t()` and `isolate()` wrap interpolated
    free text in FSI…PDI; a whole left-to-right text in a right-to-left row is `<AppText
    direction="ltr">`; the person's own text (a task title) takes `firstStrongDirection(text)`. Why:
    marks and embeddings break on one platform and in copy, search and screen readers; iOS shifted a
    wrapped English line 4.4 pt when laid right to left. (`bidi-in-source`)
12. **Prove the layout on screen, on both platforms.** Jest renders with an explicit language and
    direction and cannot see layout; every changed screen is captured in a right-to-left language
    on an iOS simulator and an Android emulator. Why: mirroring, clipping and fonts only show on a
    device.

## Workflow

1. **Detect.** Read `src/i18n/languages.ts` (i18n-strings-and-catalogs creates it): with no
   right-to-left language in `RTL_LANGUAGES`, rules 1, 2, 9 and 11 still apply (they keep a future
   right-to-left language cheap) and the checker notes that the direction rules do not apply. The
   prerequisites are i18n-strings-and-catalogs (languages, digits, bidi, the forced polyfills),
   design-system-structure (`AppText`, `TextLocaleProvider`) and architecture-and-boundaries (the
   composition root). Decide who sets the direction: the default is the app (a language setting,
   or the resolved language, decides it through the gate); an app that only ever follows the
   device language may declare `"directionSource": "device"` in `contracts/rtl.json` and skip
   step 2's gate and guard (read "An app that only follows the device" in the direction-switch
   reference first: Android then follows the device locale even when the app falls back).
2. **First time: the direction layer.** Read [references/direction-switch.md](references/direction-switch.md).
   Copy `templates/src/` and `templates/test/` to the same paths, but never overwrite
   `src/i18n/languages.ts`, `digits.ts` or `bidi.ts`: they are i18n-strings-and-catalogs' files and
   hold the app's own `LANGUAGES` and numbering; copy them only when the app has none. The
   canonical composition root already has `directionGuard`; an app whose own predates it adds it to
   `Services`, `createDeviceServices` (the kv-store adapter), `createTestServices` and their test.
   Restructure the root layout as [examples/root-layout.md](examples/root-layout.md) shows: `RootLayout` decides (adapters and the
   saved language only), `DirectionGate` renders `AppRoot`, which does everything with effects; give
   the crash fallback its own `TextLocaleProvider`. Copy `templates/contracts/rtl.json` when a
   default does not fit (physical surfaces, another icon set or root file).
3. **Building or changing a screen or component.** Read
   [references/styling-and-mirroring.md](references/styling-and-mirroring.md) and imitate
   [examples/mirrored-row.md](examples/mirrored-row.md). Strings arrive translated, numbers arrive
   formatted.
4. **Numbers, mixed-direction text, fonts.** Read
   [references/digits-bidi-and-text.md](references/digits-bidi-and-text.md): format in the model
   hook with `createNumberFormatter(localeTagFor(language, digits))`; give the person's own text its
   first-strong direction; check a bundled right-to-left font against its list.
5. **Language screens.** Follow [examples/language-choice.md](examples/language-choice.md): save
   first, then `restartForDirection` when the direction changes (first run: on Continue; Settings:
   after a Restart / Later dialog). Stop audio and flush pending work before the reload.
6. **Physical surfaces.** Wrap each in `PhysicalDirectionView` and list its component name in
   `contracts/rtl.json` `physicalSurfaces`; a Skia canvas mirrors (or not) in its own layout
   (skia-canvas-rendering).
7. **Tests.** Read [references/testing-rtl.md](references/testing-rtl.md). Keep the template tests
   green; render through `test/test-app.tsx`'s `language` (and layout `direction`) options (the
   canonical file has them) and give each new component with text or a row one right-to-left render.
8. **Run the checks** from the repo root, fix every `FAIL` line (each names the file, the rule and
   the fix) and rerun until it prints `RESULT: PASS`:
   `node ${CLAUDE_SKILL_DIR}/scripts/check-rtl.mjs .` (multi-package: add `--app-dir apps/mobile`).
   Then `npm run typecheck`, `npx eslint . --max-warnings 0` and `npx jest --ci`.
9. **Look at it.** Capture every changed screen in a right-to-left language on an iOS simulator and
   an Android emulator, each named (light and dark, phone and tablet, 200 % text), with the
   recipes and the checklist in the testing reference; run the language-switch flow
   (`templates/.maestro/flows/rtl/01-language-switch.yaml`) after any change to the language screens or
   the startup.
10. **Report** what changed, which checks passed, which SKIP or note lines remain and who creates
    their targets, and what was verified on which platform (Android restart behaviour is not yet
    device-verified by this skill: say so until you have run it).

## Definition of done

- [ ] No physical left/right style key, `row-reverse`, `textAlign` literal, hand-set
      `writingDirection`, `Text` import or `scaleX: -1` outside their one allowed file.
- [ ] With a right-to-left language and the default `directionSource` (`app`):
      `src/i18n/direction.ts` is the only `I18nManager` user, forces both flags and records the
      guard before its single reload; the root layout renders `DirectionGate` first and outermost and does no
      startup work with effects in the same component; no reload runs at import time or bypasses
      `restartForDirection` for a language change.
- [ ] `expo-localization` has `supportedLocales` and no `supportsRTL`/`forcesRTL`; navigators and
      `DirectionProvider` follow the layout; the restart screen and the crash fallback mount their
      own `TextLocaleProvider`.
- [ ] Physical surfaces sit in `PhysicalDirectionView`; `DIRECTIONAL_ICONS` is exactly the
      directional icons that exist; numbers are formatted with the locale tag; free text is
      isolated and no bidi control is hand-written.
- [ ] The template tests pass (`npx jest src/i18n src/services/direction-guard src/ui test/direction-guard --ci`),
      and each new component with text or a row has a right-to-left render.
- [ ] Right-to-left screenshots of every changed screen on iOS and Android show mirrored rows,
      start-aligned text, unflipped physical surfaces and icons, the language's digits, nothing
      clipped at 200 %.
- [ ] Expected SKIP lines only while their target is not created yet:
      `SKIP src/i18n/languages.ts [direction-module] not yet created (i18n-strings-and-catalogs creates it)`,
      `SKIP src/i18n/languages.ts [direction-before-render] not yet created (i18n-strings-and-catalogs creates it)`,
      `SKIP src/app/_layout.tsx [direction-before-render] not yet created (expo-project-bootstrap creates it)`.
      Note lines (`physical-surface-ltr: no physical surfaces declared`, the no-right-to-left-language
      note, the `directionSource is "device"` note) are information, not problems.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-rtl.mjs .` prints `RESULT: PASS`

## Anti-patterns

- **"Fixing" RTL with `row-reverse`, `isRtl ? … : …` or swapped `marginLeft`/`marginRight`.** React
  Native already swaps left and right in a right-to-left layout, so it double-flips. Use `row` and
  logical keys.
- **`left: x` for a drag thumb or scrubber.** React Native swaps it to `start` in Arabic, so the
  thumb runs backwards. Use `transform: [{ translateX: x }]` or a `PhysicalDirectionView`.
- **`paddingLeft: insets.left`.** The swap moves the notch padding to the wrong side; use
  `paddingInline: Math.max(gutter, insets.left, insets.right)` or `SafeAreaView edges`.
- **`textAlign: 'right'` for Arabic.** In a right-to-left layout `'right'` is the end edge, so the
  text lands on the left. Use `AppText align`.
- **`reloadAppAsync()` (or `Updates.reloadAsync()`) after a language change, at import time, or from
  an arbitrary module.** It skips the guard and the forced direction, and at import time it crashes
  Release builds. Use `restartForDirection` from a press handler or an effect.
- **Opening the database or hydrating stores in the component that renders the gate.** A restart
  repeats it; move it into the gate's child.
- **Deriving the layout direction from the language** (`DirectionProvider direction={directionOf(language)}`
  around the app, a navigator's `direction` from the language). Icons and navigation flip before the
  rows do; follow `readLayoutDirection()`.
- **A crash or restart screen without its own `TextLocaleProvider`.** Its Arabic text is laid out
  left to right with Latin line heights.
- **`supportsRTL: true` "to enable RTL".** React Native allows RTL by default; the flag makes the
  device language overwrite the in-app choice at every launch.
- **Flipping a whole map, a media scrubber, a clock or a play icon.** Only reading-direction arrows
  flip; a surface that mirrors maps its coordinates.
- **`String(count)` or a hard-coded locale in `Intl.NumberFormat`.** Latin digits in every language.
- **Pasting RLM, LRM or embeddings to nudge text into place.** Isolate free text, use `direction` for
  whole texts.
- **Trusting a green Jest run for layout.** Jest has no layout engine and reports left to right;
  only screenshots show mirroring.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/direction-switch.md](references/direction-switch.md) | Verified facts, the modules, deciding first in the root layout, the three flows, text vs layout direction, navigation, roots outside the providers, expo-localization, Android specifics, follow-the-device apps, React Navigation appendix, multi-package | Workflow steps 2 and 5 |
| [references/styling-and-mirroring.md](references/styling-and-mirroring.md) | Logical keys, rows and grids, text alignment, safe areas, which icons flip, the design system's RTL checklist, physical surfaces, gestures | Workflow steps 3 and 6 |
| [references/digits-bidi-and-text.md](references/digits-bidi-and-text.md) | Digit tags and outputs, numbering systems per language, numbers outside messages, the digits setting, isolation, a whole LTR text in an RTL row, the person's own text, Android first-strong, fonts for RTL scripts | Workflow step 4 |
| [references/testing-rtl.md](references/testing-rtl.md) | What Jest sees, the tests to keep, right-to-left renders, iOS simulator and Android emulator recipes (named devices), the Maestro flow, screen readers, the screenshot matrix | Workflow steps 7 and 9 |
| [examples/root-layout.md](examples/root-layout.md) | The root layout, app root, composition-root and crash-fallback changes, verified in the reference app | Workflow step 2 |
| [examples/mirrored-row.md](examples/mirrored-row.md) | A task row that mirrors, keeps the title's own direction, its test and the wrong versions the checker catches | Workflow step 3 |
| [examples/language-choice.md](examples/language-choice.md) | The Settings language model hook: save, ask, restart once | Workflow step 5 |
| `templates/src/i18n/` | `direction.ts`, `direction-plan.ts`, `direction-gate.tsx`, `direction-restart.tsx`, `direction-context.tsx` (synced; this skill owns the canonical copy), `create-number-formatter.ts`, `text-direction.ts`, each with its test; `languages.ts`, `digits.ts`, `bidi.ts` synced from i18n-strings-and-catalogs (do not edit here) | Workflow step 2 (copy) |
| `templates/src/services/direction-guard/` | The guard port, the `expo-sqlite/kv-store` adapter, the fake and its test (synced; this skill owns the canonical copy) | Workflow step 2 (copy) |
| `templates/src/ui/` | `PhysicalDirectionView` and its test | Workflow steps 2 and 6 (copy) |
| `templates/test/direction-guard/` | The adapter's test on kv-store's real SQL over `node:sqlite` (synced; this skill owns the canonical copy) | Workflow step 2 (copy) |
| `templates/contracts/` | `rtl.json` (the app's direction facts and the `directionSource` profile, every key optional) and its JSON Schema | Workflow step 2, when a default does not fit |
| `templates/.maestro/flows/rtl/01-language-switch.yaml` | Maestro flow: switch to a right-to-left language, one restart, data kept | Workflow step 9 (copy, fill the testIDs) |
| `scripts/check-rtl.mjs` | Checks the 18 rule ids named in the rules above, across the app source (routes included), config plugins and the app config | Workflow step 8, after every layout change |
| `scripts/selftest.mjs` | Proves the checker passes the templates over a small Tasks app and catches each planted bug | After changing the checker or a fixture |
| `scripts/check-lib.mjs` | Shared script helper, synced (do not edit here) | Never by hand |
| `scripts/lib/` | The source scanner and the literal reader the checker uses, synced (do not edit here) | Never by hand |
| `assets/shared.json` | Declares the synced files | When adding a shared file |
| `assets/template-placeholders.json` | Tasks-app testIDs that fill the Maestro flow when the templates are verified | When changing the flow's placeholders |
| `tests/bases/app/` | The small Tasks app the fixtures start from | When a case needs a new base file |
| `tests/fixtures/` | Overlay cases: good, one planted bug per rule, SKIP and note cases, the device profile, multi-package, bad input | When adding a rule |

## Related skills

- `i18n-strings-and-catalogs` - texts, keys, plurals, `t()`, the polyfills, languages, digits and bidi helpers.
- `design-system-structure` - `AppText`, `TextLocaleProvider`, fonts and tall-script line heights.
- `architecture-and-boundaries` - the composition root the guard joins, nothing at import time.
- `settings-and-preferences` - the saved language and digits settings and their screens.
- `error-handling-typed-results` - the root crash fallback that needs its own text locale.
- `skia-canvas-rendering` - canvas surfaces that mirror in their layout mapping.
- `accessibility-as-code` - screen-reader order, reduce motion, 200 % text.
- `unit-and-component-tests` - `test/test-app.tsx` and the render helpers.
- `e2e-maestro` - running the language-switch flow.
- `ios-simulator-build` - named simulators and the builds for the right-to-left captures.
- `android-emulator-build` - named emulators and the builds for the Android right-to-left captures.
- `app-startup-and-bootstrap` - the startup order around `DirectionGate` (RootLayout, gate, AppRoot).
- `design-visual-parity` - comparing right-to-left screens with the approved baseline.

Files in this skill

  • SKILL.md20.2 KB
  • assets/shared.json946 B
  • assets/template-placeholders.json993 B
  • examples/language-choice.md5.1 KB
  • examples/mirrored-row.md6 KB
  • examples/root-layout.md12 KB
  • references/digits-bidi-and-text.md9.7 KB
  • references/direction-switch.md23.5 KB
  • references/styling-and-mirroring.md13.4 KB
  • references/testing-rtl.md9.9 KB
  • scripts/check-rtl.mjs46.7 KB
  • scripts/lib/source-scan.mjs35.1 KB
  • scripts/lib/ts-literal.mjs6.7 KB
  • scripts/selftest.mjs486 B
  • templates/.maestro/flows/rtl/01-language-switch.yaml1.7 KB
  • templates/contracts/rtl.json822 B
  • templates/contracts/rtl.schema.json3.2 KB
  • templates/src/i18n/bidi.ts622 B
  • templates/src/i18n/create-number-formatter.test.ts1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.