Flutter Layout Widgets and Localization: Row, Column, Padding, Stack and 40 More
Layout widgets have no strings to translate, so why does localization break them? Because translated text is longer, right-to-left languages mirror the screen, and a few widgets take absolute left/right values that silently ignore the text direction. This guide covers every common layout and visual widget once, with the one rule each needs. It replaces the per-widget articles we used to publish.
The three things that actually change
- Length. German runs about 30 percent longer than English, Finnish and Russian can double a short label. Any widget with a fixed width or a single-line constraint will overflow.
- Direction. Arabic, Hebrew, Persian and Urdu are right-to-left.
MaterialAppflipsDirectionalityfor you, and most widgets follow it, but anything withleft/rightin its name does not. - Script. Tall scripts (Thai, Devanagari) need more line height; CJK text has no spaces, so
softWrapbehaves differently.
Everything below is one of these three.
Row, Column, Flex, Expanded, Flexible, Spacer
Row lays children out along the text direction, so it mirrors in RTL automatically. The trap is MainAxisAlignment.start versus a hard-coded Alignment.centerLeft on a child: the first mirrors, the second does not.
Row(
children: [
Expanded(child: Text(l10n.orderTotalLabel)), // takes the slack when the label grows
Text(l10n.price(total)),
],
)
Give the text that can grow an Expanded (or Flexible with FlexFit.loose) and leave numbers unbounded. A Spacer between them is a Flexible with no child, and it mirrors like everything else in a Row. Column has no direction issue; its only localization concern is CrossAxisAlignment.start, which means "leading edge" and flips correctly. Never use CrossAxisAlignment with a hard left in your head.
Padding, Align, Center, Positioned
EdgeInsets.only(left: 16) stays on the left in Arabic. Use EdgeInsetsDirectional.only(start: 16) and it becomes the right edge. The same pair exists for Alignment (AlignmentDirectional.centerStart) and Positioned (PositionedDirectional(start: 8)).
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.sectionTitle),
),
)
Center is Align with Alignment.center and needs nothing. Search your code for EdgeInsets.only(, Alignment.centerLeft, Alignment.topRight and Positioned(left: and convert the ones that sit next to text.
SizedBox, ConstrainedBox, LimitedBox, OverflowBox, FittedBox, AspectRatio, FractionallySizedBox, IntrinsicWidth, IntrinsicHeight, UnconstrainedBox
These set sizes, and sizes are where long translations die. The rules:
- A
SizedBox(width: 120)around aTextis a bug waiting for German. UseConstrainedBox(constraints: BoxConstraints(minWidth: 120))so the box can grow. FittedBox(fit: BoxFit.scaleDown)is the honest fix for a label that must fit one line: it shrinks the font instead of clipping. Use it for buttons and badges, not for body text.IntrinsicWidthmakes a column of buttons as wide as the widest translated label. It costs a second layout pass, so use it on small groups, not in lists.AspectRatioandFractionallySizedBoxscale with the parent, not with text, so they are safe.LimitedBoxonly acts when unconstrained (inside aListView), same story.OverflowBoxandUnconstrainedBoxlet a child exceed its parent, which is how you get clipped Arabic text on the right edge; avoid them around text.
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 120, minHeight: 44),
child: FilledButton(
onPressed: save,
child: FittedBox(fit: BoxFit.scaleDown, child: Text(l10n.saveChanges)),
),
)
Stack, IndexedStack, Wrap, LayoutBuilder
Stack children with Positioned(left:) do not mirror; use PositionedDirectional. A badge pinned to the top-right corner of an avatar belongs at top: 0, end: 0 so it moves to the top-left in Hebrew. IndexedStack is a Stack that shows one child, same rules.
Wrap is the widget that saves you when a row of chips or buttons no longer fits in Spanish: it wraps to a second line instead of overflowing. Wrap follows text direction, so chips flow right-to-left in Arabic.
LayoutBuilder lets you switch between a Row and a Column based on the available width, which is the right answer when two translated buttons stop fitting side by side:
LayoutBuilder(builder: (context, constraints) {
final stacked = constraints.maxWidth < 360;
final buttons = [cancelButton, confirmButton];
return stacked
? Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: buttons)
: Row(mainAxisAlignment: MainAxisAlignment.end, children: buttons);
})
Container, DecoratedBox, ColoredBox, PhysicalModel, Divider, Baseline
Container combines padding, alignment and decoration; each part follows the rule above (EdgeInsetsDirectional, AlignmentDirectional). BoxDecoration borders are the exception people miss: Border(left: ...) is physical. Use BorderDirectional(start: ...) for an accent bar next to text. BorderRadius.only(topLeft:) has BorderRadiusDirectional.only(topStart:).
ColoredBox, DecoratedBox and PhysicalModel have no text and no direction, nothing to do. Divider has indent and endIndent, which are already directional. Baseline aligns text baselines across scripts; give it TextBaseline.alphabetic for Latin and Cyrillic and TextBaseline.ideographic when the locale is Chinese, Japanese or Korean.
Transform, RotatedBox, FractionalTranslation, Opacity, Visibility, Offstage, IgnorePointer, AbsorbPointer
Transforms are in physical pixels and do not mirror. A chevron icon rotated to point "forward" with Transform.rotate points the wrong way in RTL. Use Icons.arrow_forward (which Flutter mirrors through matchTextDirection) or multiply the angle by Directionality.of(context) == TextDirection.rtl ? -1 : 1. RotatedBox has the same issue; FractionalTranslation has an x that you negate in RTL.
Opacity, Visibility, Offstage, IgnorePointer and AbsorbPointer change nothing about layout direction or text. The one localization point: a widget hidden with Visibility(visible: false) still has its text in the semantics tree unless maintainSemantics is false, so screen readers in another language may read hidden labels.
Clip and filter widgets: ClipRect, ClipRRect, ClipOval, ClipPath, BackdropFilter, ColorFiltered, ImageFiltered, ShaderMask, CustomPaint, RepaintBoundary
None of these care about the locale. The two things worth knowing:
ClipRRectwithBorderRadius.only(topLeft:)is physical. UseBorderRadiusDirectionalif the rounded corner is meant to sit on the leading side.CustomPaintis where you draw text yourself withTextPainter. PasstextDirection: Directionality.of(context)and the locale'sTextStyleor the paragraph will not shape Arabic correctly.RepaintBoundaryis a performance tool; wrap aCustomPaintthat renders translated text in one so a language switch repaints only that subtree.
final painter = TextPainter(
text: TextSpan(text: label, style: style),
textDirection: Directionality.of(context),
locale: Localizations.localeOf(context),
)..layout(maxWidth: size.width);
Ink, InkWell, GestureDetector, InteractiveViewer
Touch widgets do not localize, but their semantics do. InkWell and GestureDetector have no label of their own, so wrap the child in Semantics(label: l10n.openProfile, button: true) or the screen reader announces nothing. InteractiveViewer pans in physical coordinates and that is correct, maps do not mirror.
A checklist you can run in one afternoon
- Grep for
EdgeInsets.only(,Alignment.center(Left|Right),Positioned(left|right:,Border(left|right:and swap for the directional versions where the widget sits next to text. - Grep for
SizedBox(width:around aTextand change to aConstrainedBoxminimum or aFittedBox. - Run the app with
locale: Locale('de')and look for overflow stripes. ThenLocale('ar')and look for anything still pinned to the left. - Turn on
MaterialApp(debugShowMaterialGrid: true)or the Widget Inspector's "Show guidelines" to see which boxes are fixed.
Related
- Flutter RTL support: Arabic, Hebrew and Persian layouts that work
- Buttons, chips, inputs and text widgets
- Lists, grids and scrolling widgets
- How to translate a Flutter app, step by step
Translated strings that overflow are a layout problem; strings that are missing are a workflow problem. FlutterLocalisation handles the second one: AI translation per key, review per language, and the ARB files committed to your repo.