← Back to Blog

Flutter Navigation, Dialogs and App Structure Widgets: Localization Guide

flutternavigationdialogswidgetslocalization

Flutter Navigation, Dialogs and App Structure Widgets: Localization Guide

The scaffold, the app bar, the drawer, the tabs, the dialogs and the snackbars are the widgets every screen shares, so a localization mistake in one of them shows up everywhere. This guide covers each structural widget once: which strings it owns, which ones flutter_localizations already translates, and what flips in right-to-left languages. It replaces the per-widget posts we used to publish.

Scaffold and AppBar

Scaffold itself has no text. The AppBar has a title, actions and an automatic leading button. Three rules:

  • The title is one key. centerTitle defaults to false on Android and true on iOS; left unset, a start-aligned title moves to the right in Arabic by itself.
  • The back button and the drawer button get their tooltips ("Back", "Open navigation menu") from flutter_localizations. You only need the delegates in MaterialApp.
  • Actions are icon buttons and need a tooltip each, or screen readers announce nothing.
AppBar(
  title: Text(context.l10n.ordersTitle),
  actions: [
    IconButton(
      tooltip: context.l10n.searchOrders,
      icon: const Icon(Icons.search),
      onPressed: openSearch,
    ),
  ],
)

Long titles in German overflow the bar. Set maxLines: 1 and overflow: TextOverflow.ellipsis on the title Text, or use a SliverAppBar with a flexible space (see the lists and scrolling guide).

Drawer, NavigationDrawer, NavigationBar, NavigationRail, BottomAppBar, TabBar

All of these contain a list of labelled destinations. The label is a key; the icon is not. The drawer opens from the leading edge, which is the right side in RTL, and NavigationRail sits on the leading side, both automatic.

NavigationBar(
  selectedIndex: index,
  onDestinationSelected: select,
  destinations: [
    NavigationDestination(icon: const Icon(Icons.home_outlined), label: context.l10n.navHome),
    NavigationDestination(icon: const Icon(Icons.search), label: context.l10n.navSearch),
    NavigationDestination(icon: const Icon(Icons.person_outline), label: context.l10n.navProfile),
  ],
)

Bottom navigation labels are the strings most likely to be too long: "Einstellungen" does not fit under an icon at the default text scale on a small phone. Keep nav labels to one short word per language, and use labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected if a language still overflows. TabBar has isScrollable: true for the same reason; with four German tabs on a narrow screen it is the only option. BottomAppBar holds buttons and follows the buttons guide.

Drawer has one more string: the semantics label "Navigation menu" from Material localizations, already translated.

Navigator and routes

Navigator has no visible text, but routes do: the title of a MaterialPageRoute is used by iOS back buttons and by the Android task switcher. Set it, and set MaterialApp.onGenerateTitle so the app title in the OS switcher follows the locale instead of staying in English:

MaterialApp(
  onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
  ...
)

Route names (/settings) are identifiers, never translate them. For go_router with localized deep links see go_router localization.

Dialogs: AlertDialog, SimpleDialog, Dialog, AboutDialog, BottomSheet

AlertDialog has title, content and actions. The actions are buttons with keys. The order of "Cancel" and "OK" is a platform convention (confirm on the trailing side on Material) and actions already lay out in text direction, so you do not reorder them for RTL.

showDialog<bool>(
  context: context,
  builder: (context) => AlertDialog(
    title: Text(context.l10n.deleteOrderTitle),
    content: Text(context.l10n.deleteOrderBody(order.number)),
    actions: [
      TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.cancel)),
      FilledButton(onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.delete)),
    ],
  ),
);

The context inside builder is a new one under the dialog route, and it still has Localizations above it, so context.l10n works. SimpleDialog lists options; each SimpleDialogOption child is a key. Dialog is the bare container.

AboutDialog and showAboutDialog show the app name, version, legalese and a "View licenses" button. The button label and the license page come from flutter_localizations; applicationName and applicationLegalese are yours, and the legalese often needs a per-locale legal text rather than a translation, so keep it as a key with a note for the translator.

showModalBottomSheet has no text of its own. Its drag handle and the "Dismiss" semantics are translated by Material. Content inside follows the normal rules.

Feedback: SnackBar, MaterialBanner

Both show a message and optional action. The message is a key; so is the action label. The gotcha is timing: a snackbar built from a context that is gone (after Navigator.pop) throws. Use ScaffoldMessenger.of(context) from the screen that stays, or a global scaffoldMessengerKey, and resolve the string before the async gap:

final message = context.l10n.orderDeleted;  // resolve first
await repository.delete(order);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
  content: Text(message),
  action: SnackBarAction(label: context.l10n.undo, onPressed: undo),
));

MaterialBanner is the persistent version with content and actions; same rules. Both wrap long text automatically, so a two-line Finnish message is fine.

Card, ListTile in cards, Stepper, PopupMenuButton

Card has no text. Stepper has step titles and subtitles (keys) and the built-in "Continue" and "Cancel" controls; override controlsBuilder if you want product wording, otherwise those two come from Material localizations. PopupMenuButton items are keys, the "Show menu" tooltip is translated for you, and the menu opens on the trailing side in RTL.

Settings screens and legal pages

A settings screen is a ListView of ListTile and SwitchListTile rows, all keys. Two localization-specific rows belong there: a language picker (see changing the language at runtime) and the legal links. Terms and privacy texts are documents, not UI strings; serve them from a URL per locale or from a markdown asset per locale rather than from the ARB file, which is meant for short strings.

Checklist

  1. onGenerateTitle is set, so the OS task switcher shows the translated app name.
  2. Every AppBar action and every IconButton has a tooltip key.
  3. Navigation labels are one short word per language; TabBar is scrollable when it has more than three tabs.
  4. Dialog actions are keys, in the default order.
  5. Snackbar strings are resolved before any await.
  6. Legal texts are per-locale documents, not ARB keys.

Related

Structural strings change rarely, which makes them easy to forget when you add a language. FlutterLocalisation shows per-language coverage so a new locale ships with every nav label and dialog button filled in.