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?
answer
- hunt for left and right in code
- LinearGradient defaults to centerLeft
- Icon mirrors only if matchTextDirection
- custom painters read Directionality
- golden tests in both directions
basics
~10 sSearch 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 sRTL 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 linesimport '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
Recognise the suspects: EdgeInsets.only(left:), Alignment.centerLeft, TextAlign.left and BorderRadius topLeft, and their directional replacements.
Explain why LinearGradient, custom icons and painters do not mirror by default, and how matchTextDirection and Transform.flip handle glyphs.
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.
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.