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
DateFormatwith 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
GlobalCupertinoLocalizations.delegateis in the delegate list.use24hFormatcomes fromMediaQuery, not a constant.- Every
CupertinoPageRoutehas a localizedtitle, so back buttons read correctly. - Segmented controls and tab bars use short labels and enum keys.
CFBundleLocalizationslists every language you ship.
Related
- Navigation, dialogs and app structure (Material)
- Buttons, chips, inputs and text widgets
- Flutter DateFormat by locale
- Native iOS apps with Xcode String Catalogs
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.