← Back to Blog

Flutter CupertinoDatePicker Localization: iOS Date Pickers for Multilingual Apps

fluttercupertinodatepickerioslocalizationrtl

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

  1. Use use24hFormat from MediaQuery.of(context).alwaysUse24HourFormat when using dateAndTime mode so the time format matches the device setting and locale convention.

  2. Set minimumDate and maximumDate to prevent users from selecting invalid dates, and translate any error messages that appear when dates are out of range.

  3. Present in modal sheets using showCupertinoModalPopup with localized Cancel and Done buttons for a consistent iOS experience.

  4. Format displayed dates using intl package's DateFormat with the active locale so the selected date appears in the user's expected format outside the picker.

  5. Test with multiple locales including Arabic (RTL), German (long month names), and Japanese (different date ordering) to verify the picker adapts correctly.

  6. 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.

Further Reading