skip to content

The Arabic and Hebrew build of a Flutter currency converter shows padding, a gradient and a swap arrow on the wrong side; how do you find and fix these RTL bugs?

level: seniorimportance: should knowfreq 32%

answer

  1. hunt for left and right in code
  2. LinearGradient defaults to centerLeft
  3. Icon mirrors only if matchTextDirection
  4. custom painters read Directionality
  5. golden tests in both directions

basics

~10 s

Search for absolute geometry (EdgeInsets.only(left:), Alignment.centerLeft, TextAlign.left, BorderRadius topLeft, LinearGradient's centerLeft default) and replace it with directional types; mirror custom icons and painters from Directionality.of, and pin both directions with widget or golden tests.

solid answer

~30 s

RTL bugs are almost always hard-coded physical sides. Replace `EdgeInsets.only(left:)` and `fromLTRB` with `EdgeInsetsDirectional`, `Alignment.centerLeft` with `AlignmentDirectional.centerStart`, `TextAlign.left` with `TextAlign.start`, and `BorderRadius.only(topLeft:)` with `BorderRadiusDirectional`. A `LinearGradient` defaults to `begin: Alignment.centerLeft, end: Alignment.centerRight`, so a progress fill starts on the wrong side unless you pass `AlignmentDirectional` values. An `Icon` mirrors in `rtl` only when its `IconData.matchTextDirection` is true, as for `Icons.arrow_back`; custom icon fonts, images and `CustomPainter` arrows must read `Directionality.of(context)` and flip, for example with `Transform.flip(flipX: ...)`. Also remove manual list reversals, since `Row` already mirrors. Lock the fix in with widget and golden tests pumped in both directions.

code

dart · 35 lines
dart
import 'package:flutter/material.dart';

class RateBar extends StatelessWidget {
  const RateBar({super.key, required this.fraction});

  final double fraction;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    final isRtl = Directionality.of(context) == TextDirection.rtl;
    return Row(
      children: [
        Transform.flip(
          flipX: isRtl,
          child: Image.asset('assets/icons/swap_chevron.png', width: 20),
        ),
        const SizedBox(width: 8),
        Expanded(
          child: Container(
            height: 8,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: AlignmentDirectional.centerStart,
                end: AlignmentDirectional.centerEnd,
                colors: [colors.primary, colors.surfaceContainerHighest],
                stops: [fraction, fraction],
              ),
            ),
          ),
        ),
      ],
    );
  }
}

go deeper

for a junior

Recognise the suspects: EdgeInsets.only(left:), Alignment.centerLeft, TextAlign.left and BorderRadius topLeft, and their directional replacements.

for a middle

Explain why LinearGradient, custom icons and painters do not mirror by default, and how matchTextDirection and Transform.flip handle glyphs.

for a senior

Run a systematic hunt across code and assets, fix painters by passing direction in, keep left-to-right islands, and add widget and golden tests in both directions to CI.

for a principal

Budget the RTL retrofit across teams and make directional geometry and dual-direction tests part of the definition of done.

## Why these bugs appear Flutter mirrors a lot for free: `Row` order, `Text` alignment, Material widgets' leading and trailing slots, and every directional geometry value. What it cannot mirror is code that names a **physical side**. An app written in English first accumulates `left`, `right`, `centerLeft` and `topLeft` in dozens of places, and each one is a candidate bug in Arabic and Hebrew. ## Finding them 1. **Run the app right-to-left.** Set the device to Hebrew or Arabic, or temporarily pass `locale: const Locale('he')` to `MaterialApp`, and walk every screen. 2. **Search the code** for the usual suspects: `EdgeInsets.only(`, `EdgeInsets.fromLTRB(`, `Alignment.centerLeft`, `Alignment.topRight`, `TextAlign.left`, `TextAlign.right`, `BorderRadius.only(`, `LinearGradient(` without `begin`, and any `reversed` applied to children "for RTL". 3. **Check assets and painters**: custom icon fonts, PNG or SVG arrows, `CustomPainter` code that draws with fixed x coordinates. 4. **Pin the result with tests** so the next English-only change cannot undo it (below). ## The common culprits and their fixes | Symptom in the converter | Cause | Fix | |---|---|---| | Flag and currency code hug the left edge | `EdgeInsets.only(left: 16)` | `EdgeInsetsDirectional.only(start: 16)` | | Amount column aligned to the wrong side | `TextAlign.right` | `TextAlign.end` | | Rounded leading corners on the wrong side | `BorderRadius.only(topLeft: ...)` | `BorderRadiusDirectional.only(topStart: ...)` | | Rate-progress bar fills from the left | `LinearGradient` defaults `begin: Alignment.centerLeft` | `begin: AlignmentDirectional.centerStart, end: AlignmentDirectional.centerEnd` | | "Swap" arrow drawn by a painter points the wrong way | Fixed coordinates in `paint` | Pass the direction in and mirror the path | | Back arrow correct, custom chevron not | Only icons with `matchTextDirection: true` mirror | Mirror the custom glyph explicitly | | Order of two fields flipped twice | Children reversed manually on top of `Row`'s own mirroring | Delete the manual reversal | `LinearGradient.createShader` resolves `begin` and `end` with the text direction it receives, so directional alignments mirror as soon as you use them. ## Icons, images and painters - **`Icon`** checks `IconData.matchTextDirection`. Material's directional icons such as `Icons.arrow_back` and `Icons.arrow_forward` set it, so `Icon` flips them horizontally in `rtl`. Icons without the flag never flip. - **Custom glyphs and images** need an explicit decision. Wrap them in `Transform.flip(flipX: Directionality.of(context) == TextDirection.rtl, child: ...)`. Which glyphs *should* flip (a back arrow does, a clock does not) is a design rule; the Flutter job is to apply that rule consistently. - **`CustomPainter`** has no ambient context. Read `Directionality.of(context)` in the widget that creates the painter, pass it as a field, and include it in `shouldRepaint`, so the drawing mirrors and repaints when the direction changes. ## Scrolling, pages and gestures - Horizontal `ListView`, `PageView` and other scrollables take their axis direction from `Directionality`, so a carousel of currencies starts at the right edge and swipes the other way in Arabic. Code that also passes `reverse: true` for RTL undoes it. - `Dismissible` directions such as `DismissDirection.startToEnd` are logical, so a swipe-to-delete that uses `startToEnd` and `endToStart` mirrors correctly, while hard-coded pixel offsets in custom drag handlers do not. - Animations built with `Offset` tweens (a panel sliding in from `Offset(-1, 0)`) are physical; derive the sign from the ambient direction. ## Content that must stay left-to-right Not every mirror is a fix. Exchange-rate formulas, IBANs and card numbers read left to right in every language; keep them in a small `Directionality(textDirection: TextDirection.ltr)` island or set `textDirection` on that one `Text` or `TextField`, instead of rolling back the screen's mirroring. ## Locking it in with tests - **Widget tests** can pump a screen under `Directionality(textDirection: TextDirection.rtl, child: ...)`, or under `MaterialApp(locale: const Locale('ar'), ...)` with the real delegates, then assert positions with `tester.getTopLeft` and `tester.getTopRight`, for example that the flag's right edge is at the row's right edge. - **Golden tests** in both directions catch gradients, painters and icons that position checks miss. - Run the RTL variants in CI next to the LTR ones, so a new `EdgeInsets.only(left:)` fails a build rather than a release.

  • Why does Icons.arrow_back flip in Arabic while a custom chevron from your icon font does not?
    `Icon` mirrors only glyphs whose `IconData.matchTextDirection` is true, and Material's directional icons such as `arrow_back` set it. A custom `IconData` defaults the flag to false; set it in the `IconData` constructor or wrap the widget in `Transform.flip`.
  • How do you make a CustomPainter mirror correctly?
    Read `Directionality.of(context)` in the widget that builds the `CustomPaint`, pass the direction to the painter as a field, draw from the start edge based on it, and compare it in `shouldRepaint` so a direction change repaints.
  • Should the rate formula '1 USD = 3.71 ILS' mirror in the Hebrew build?
    No. It is left-to-right content, so keep it in a small `Directionality(textDirection: TextDirection.ltr)` island or set `textDirection` on that `Text`, while the surrounding layout stays right-to-left.

saying these in an interview costs you the question

  • Every Material icon flips automatically in right-to-left locales.
  • LinearGradient follows the text direction by default.
  • Reverse Row children in Arabic so the order looks right.
  • CustomPainter reads Directionality from its own BuildContext.
  • Golden tests in the default direction are enough to cover RTL.