Flutter CupertinoDatePicker Localization: iOS Date Pickers for Multilingual Apps
CupertinoDatePicker is a Flutter widget that renders an iOS-style scrolling date picker with spinning wheels for day, month, and year selection. In multilingual applications, CupertinoDatePicker is essential for displaying month names and weekday labels in the active language, formatting dates according to locale-specific conventions (day/month/year vs month/day/year), supporting RTL text alignment within picker columns, and providing accessible date selection announcements in the user's language.
Understanding CupertinoDatePicker in Localization Context
CupertinoDatePicker renders spinning wheel columns for date components following iOS design patterns. For multilingual apps, this enables:
- Translated month names (January, Janvier, يناير) in picker wheels
- Locale-aware date ordering (DD/MM/YYYY for Europe, MM/DD/YYYY for US)
- RTL-compatible column layouts for Arabic and Hebrew
- Accessible value announcements in the active language
Why CupertinoDatePicker Matters for Multilingual Apps
CupertinoDatePicker provides:
- Native iOS experience: Date picking that matches the platform convention in every language
- Automatic month translation: Month and weekday names rendered in the active locale
- Locale-aware ordering: Day, month, and year columns arranged per regional convention
- Accessibility: VoiceOver reads date values in the correct language
Basic CupertinoDatePicker Implementation
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedCupertinoDatePickerExample extends StatefulWidget {
const LocalizedCupertinoDatePickerExample({super.key});
@override
State<LocalizedCupertinoDatePickerExample> createState() =>
_LocalizedCupertinoDatePickerExampleState();
}
class _LocalizedCupertinoDatePickerExampleState
extends State<LocalizedCupertinoDatePickerExample> {
DateTime _selectedDate = DateTime.now();
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.selectDateTitle),
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.selectedDateLabel(_formatDate(_selectedDate)),
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: _selectedDate,
minimumDate: DateTime(2020),
maximumDate: DateTime(2030),
onDateTimeChanged: (DateTime newDate) {
setState(() {
_selectedDate = newDate;
});
},
),
),
],
),
),
);
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}
Advanced CupertinoDatePicker Patterns for Localization
Birthday Selector with Age Calculation
CupertinoDatePicker configured for birthday selection with a localized age display.
class BirthdayPickerExample extends StatefulWidget {
const BirthdayPickerExample({super.key});
@override
State<BirthdayPickerExample> createState() => _BirthdayPickerExampleState();
}
class _BirthdayPickerExampleState extends State<BirthdayPickerExample> {
DateTime _birthday = DateTime(2000, 1, 1);
int get _age {
final now = DateTime.now();
int age = now.year - _birthday.year;
if (now.month < _birthday.month ||
(now.month == _birthday.month && now.day < _birthday.day)) {
age--;
}
return age;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.birthdayTitle),
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
l10n.yourAgeLabel(_age),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
l10n.birthdaySubtitle,
style: TextStyle(
fontSize: 14,
color: CupertinoColors.systemGrey.resolveFrom(context),
),
),
],
),
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: _birthday,
minimumDate: DateTime(1920),
maximumDate: DateTime.now(),
onDateTimeChanged: (DateTime date) {
setState(() {
_birthday = date;
});
},
),
),
],
),
),
);
}
}
Date and Time Combined Picker
CupertinoDatePicker in dateAndTime mode for scheduling with localized labels.
class SchedulePickerExample extends StatefulWidget {
const SchedulePickerExample({super.key});
@override
State<SchedulePickerExample> createState() => _SchedulePickerExampleState();
}
class _SchedulePickerExampleState extends State<SchedulePickerExample> {
DateTime _scheduledDateTime = DateTime.now().add(const Duration(hours: 1));
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.scheduleTitle),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {
// Confirm scheduling
},
child: Text(l10n.confirmButton),
),
),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.scheduleDescription,
style: const TextStyle(fontSize: 16),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6.resolveFrom(context),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(CupertinoIcons.calendar),
const SizedBox(width: 8),
Text(
l10n.scheduledForLabel(
_scheduledDateTime.toString().substring(0, 16),
),
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
const SizedBox(height: 16),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: _scheduledDateTime,
minimumDate: DateTime.now(),
maximumDate: DateTime.now().add(const Duration(days: 365)),
use24hFormat: MediaQuery.of(context).alwaysUse24HourFormat,
onDateTimeChanged: (DateTime dateTime) {
setState(() {
_scheduledDateTime = dateTime;
});
},
),
),
],
),
),
);
}
}
Modal Date Picker Sheet
A CupertinoDatePicker presented in a modal bottom sheet with localized action buttons.
class ModalDatePickerExample extends StatefulWidget {
const ModalDatePickerExample({super.key});
@override
State<ModalDatePickerExample> createState() => _ModalDatePickerExampleState();
}
class _ModalDatePickerExampleState extends State<ModalDatePickerExample> {
DateTime? _selectedDate;
void _showDatePicker(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
DateTime tempDate = _selectedDate ?? DateTime.now();
showCupertinoModalPopup(
context: context,
builder: (BuildContext popupContext) {
return Container(
height: 300,
color: CupertinoColors.systemBackground.resolveFrom(context),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
child: Text(l10n.cancelButton),
onPressed: () => Navigator.pop(popupContext),
),
CupertinoButton(
child: Text(l10n.doneButton),
onPressed: () {
setState(() {
_selectedDate = tempDate;
});
Navigator.pop(popupContext);
},
),
],
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: tempDate,
onDateTimeChanged: (DateTime date) {
tempDate = date;
},
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.datePickerTitle),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_selectedDate != null
? l10n.selectedDateLabel(
_selectedDate.toString().substring(0, 10))
: l10n.noDateSelected,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
CupertinoButton.filled(
onPressed: () => _showDatePicker(context),
child: Text(l10n.chooseDateButton),
),
],
),
),
);
}
}
RTL Support and Bidirectional Layouts
CupertinoDatePicker automatically handles RTL text direction for month names and labels within the spinning wheels. The column order may remain consistent with iOS conventions, but text within each column aligns correctly for RTL languages.
class BidirectionalDatePickerExample extends StatelessWidget {
const BidirectionalDatePickerExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.selectDateTitle),
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsetsDirectional.all(16),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
l10n.datePickerInstructions,
style: const TextStyle(fontSize: 16),
),
),
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: DateTime.now(),
onDateTimeChanged: (DateTime date) {},
),
),
],
),
),
);
}
}
Testing CupertinoDatePicker Localization
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
Widget buildTestWidget({Locale locale = const Locale('en')}) {
return CupertinoApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedCupertinoDatePickerExample(),
);
}
testWidgets('CupertinoDatePicker renders in English', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(CupertinoDatePicker), findsOneWidget);
});
testWidgets('CupertinoDatePicker works in RTL locale', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('CupertinoDatePicker renders in German', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('de')));
await tester.pumpAndSettle();
expect(find.byType(CupertinoDatePicker), findsOneWidget);
});
}
Best Practices
Use
use24hFormatfromMediaQuery.of(context).alwaysUse24HourFormatwhen usingdateAndTimemode so the time format matches the device setting and locale convention.Set
minimumDateandmaximumDateto prevent users from selecting invalid dates, and translate any error messages that appear when dates are out of range.Present in modal sheets using
showCupertinoModalPopupwith localized Cancel and Done buttons for a consistent iOS experience.Format displayed dates using
intlpackage'sDateFormatwith the active locale so the selected date appears in the user's expected format outside the picker.Test with multiple locales including Arabic (RTL), German (long month names), and Japanese (different date ordering) to verify the picker adapts correctly.
Provide context labels above the picker explaining what date is being selected, translated into the active language.
Conclusion
CupertinoDatePicker provides iOS-style date selection for Flutter apps with built-in locale awareness. For multilingual apps, it automatically translates month names, adjusts date column ordering per locale, and supports RTL text alignment. By presenting pickers in modal sheets with localized action buttons, formatting displayed dates with the intl package, and testing across diverse locales, you can build date selection flows that feel native to iOS users in every supported language.