← Back to Blog

Flutter Cupertino Widgets and Localization: Pickers, Sheets, Dialogs, Navigation

fluttercupertinoioswidgetslocalization

Flutter Cupertino Widgets and Localization: Pickers, Sheets, Dialogs, Navigation

Cupertino widgets give a Flutter app the iOS look, and they come with their own localization delegate, their own date and time formats, and a few traps that Material widgets do not have. This guide covers every Cupertino widget with a localization angle in one place. It replaces the per-widget posts we used to publish.

One delegate you must add

GlobalCupertinoLocalizations.delegate is what translates the Cupertino built-in strings: "Cancel" and "Done" on pickers, "Copy" and "Paste" in text fields, the "Today" label of the date picker, the alert and action sheet semantics. gen-l10n's AppLocalizations.localizationsDelegates already includes it. If you build the list by hand, you need all three:

CupertinoApp(
  localizationsDelegates: const [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: AppLocalizations.supportedLocales,
)

Without the Cupertino delegate, CupertinoDatePicker throws "No CupertinoLocalizations found" in any locale except English. If you use MaterialApp with Cupertino widgets inside, the same list applies.

CupertinoDatePicker, CupertinoTimerPicker, CupertinoPicker

CupertinoDatePicker renders month names, day-of-week abbreviations and the AM/PM markers through CupertinoLocalizations, and it orders the columns per locale (day before month in German, year first in Japanese). You do not touch any of that. What you do control:

  • use24hFormat: read it from the device, do not hard-code. MediaQuery.alwaysUse24HourFormatOf(context) gives you the user's system setting.
  • The "Done" button above the picker is yours; use a key.
  • The selected value you display elsewhere goes through DateFormat with the current locale.
CupertinoDatePicker(
  mode: CupertinoDatePickerMode.dateAndTime,
  use24hFormat: MediaQuery.alwaysUse24HourFormatOf(context),
  initialDateTime: value,
  onDateTimeChanged: setValue,
)

CupertinoTimerPicker shows "hours", "min" and "sec" labels, also from the delegate. CupertinoPicker is the bare wheel; the items are your Text widgets with keys, and a CupertinoPicker of countries needs locale-aware sorting of the names.

CupertinoAlertDialog, CupertinoActionSheet, CupertinoContextMenu

CupertinoAlertDialog has title, content and actions. iOS puts the cancel action last and bold, which CupertinoDialogAction(isDefaultAction: true) and isDestructiveAction handle; you only supply keys.

showCupertinoDialog<void>(
  context: context,
  builder: (context) => CupertinoAlertDialog(
    title: Text(context.l10n.signOutTitle),
    content: Text(context.l10n.signOutBody),
    actions: [
      CupertinoDialogAction(onPressed: () => Navigator.pop(context), child: Text(context.l10n.cancel)),
      CupertinoDialogAction(isDestructiveAction: true, onPressed: signOut, child: Text(context.l10n.signOut)),
    ],
  ),
);

CupertinoActionSheet has the same shape with cancelButton separate. CupertinoContextMenu shows actions on long press; each CupertinoContextMenuAction child is a key and the trailing icon is mirrored in RTL.

CupertinoNavigationBar, CupertinoTabBar, CupertinoPageScaffold

CupertinoNavigationBar has middle (the title), leading and trailing. The automatic back button shows the previous route's title, truncated to "Back" when too long, and both the title and the word "Back" come from the previous CupertinoPageRoute(title:) and the delegate. So set title on every CupertinoPageRoute, with a key:

Navigator.push(context, CupertinoPageRoute(
  title: context.l10n.settingsTitle,
  builder: (_) => const SettingsPage(),
));

In RTL the back chevron and the slide-in transition mirror on their own. CupertinoTabBar items have label keys; the same "one short word per language" rule as Material bottom navigation applies, and there is no scrollable variant, so five tabs plus German is a problem to solve at design time. CupertinoPageScaffold has no text.

CupertinoTextField, CupertinoSearchTextField, CupertinoFormSection, CupertinoListSection, CupertinoListTile

CupertinoTextField has placeholder (a key) and prefix/suffix. The clear button and the context menu ("Cut", "Copy", "Paste", "Select All") are translated by the delegate. CupertinoSearchTextField has placeholder defaulting to the localized "Search", so you can leave it unset.

CupertinoFormSection and CupertinoListSection take a header and footer (keys) and children. CupertinoListTile mirrors ListTile: title, subtitle, additionalInfo, leading, trailing, with leading and trailing swapping in RTL.

CupertinoFormSection.insetGrouped(
  header: Text(context.l10n.accountSection),
  children: [
    CupertinoTextFormFieldRow(
      prefix: Text(context.l10n.emailLabel),
      placeholder: context.l10n.emailHint,
      validator: (v) => v != null && v.contains('@') ? null : context.l10n.emailInvalid,
    ),
  ],
)

CupertinoButton, CupertinoSwitch, CupertinoSlidingSegmentedControl, CupertinoSegmentedControl

CupertinoButton takes a child, usually Text(key). It has no tooltip; wrap an icon-only button in Semantics(label:). CupertinoSwitch has no label of its own; pair it with a CupertinoListTile.

CupertinoSlidingSegmentedControl and CupertinoSegmentedControl map keys to labels. The segments lay out in text direction and have equal width, so one long German segment stretches all of them. Keep segment labels short or switch to a CupertinoTabBar. Use a stable enum as the map key, never the translated label:

CupertinoSlidingSegmentedControl<Period>(
  groupValue: period,
  onValueChanged: (v) => setPeriod(v!),
  children: {
    Period.week: Text(context.l10n.periodWeek),
    Period.month: Text(context.l10n.periodMonth),
    Period.year: Text(context.l10n.periodYear),
  },
)

The native side: Info.plist

Everything above is Flutter. iOS itself also decides which languages your app supports, from CFBundleLocalizations in Info.plist. If it lists only English, the system keeps the app in English however many ARB files you ship, and permission prompts stay English. The fix is a five-line plist change, covered in Flutter app stuck in English on iOS. The permission prompt texts (NSCameraUsageDescription and friends) live in InfoPlist.strings or an InfoPlist.xcstrings catalog, not in ARB; FlutterLocalisation can manage those from the same project.

Checklist

  1. GlobalCupertinoLocalizations.delegate is in the delegate list.
  2. use24hFormat comes from MediaQuery, not a constant.
  3. Every CupertinoPageRoute has a localized title, so back buttons read correctly.
  4. Segmented controls and tab bars use short labels and enum keys.
  5. CFBundleLocalizations lists every language you ship.

Related

Cupertino apps often ship next to a native Swift app. FlutterLocalisation keeps both on one set of keys: ARB files for Flutter, a String Catalog for Xcode, translated together.