← Back to Blog

Flutter CupertinoSwitch Localization: iOS-Style Toggles for Multilingual Apps

fluttercupertinoswitchioslocalizationrtl

Flutter CupertinoSwitch Localization: iOS-Style Toggles for Multilingual Apps

CupertinoSwitch is a Flutter widget that provides an iOS-style toggle switch for boolean settings. In multilingual applications, CupertinoSwitch is essential for building iOS-native settings screens with translated toggle labels, providing localized on/off state descriptions for accessibility, handling locale-specific default values for regional preferences, and maintaining iOS toggle conventions across all supported locales.

Understanding CupertinoSwitch in Localization Context

CupertinoSwitch renders an iOS-style toggle that slides between on and off states with a smooth animation. For multilingual apps, this enables:

  • Translated setting labels paired with iOS-native toggles
  • Localized accessibility announcements for on/off state changes
  • Settings grouped in iOS-style sections with translated headers
  • Dependent toggles with translated explanatory text

Why CupertinoSwitch Matters for Multilingual Apps

CupertinoSwitch provides:

  • iOS-native appearance: Toggle styling that matches iOS system settings
  • Accessibility: VoiceOver announces on/off state in the active language
  • Settings integration: Pairs with CupertinoListTile for iOS-style settings rows
  • State feedback: Visual on/off state that's universally understood

Basic CupertinoSwitch Implementation

import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedCupertinoSwitchExample extends StatefulWidget {
  const LocalizedCupertinoSwitchExample({super.key});

  @override
  State<LocalizedCupertinoSwitchExample> createState() =>
      _LocalizedCupertinoSwitchExampleState();
}

class _LocalizedCupertinoSwitchExampleState
    extends State<LocalizedCupertinoSwitchExample> {
  bool _notifications = true;
  bool _darkMode = false;
  bool _analytics = false;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.settingsTitle),
      ),
      child: SafeArea(
        child: CupertinoListSection.insetGrouped(
          header: Text(l10n.preferencesSection),
          children: [
            CupertinoListTile(
              title: Text(l10n.notificationsLabel),
              trailing: CupertinoSwitch(
                value: _notifications,
                onChanged: (value) {
                  setState(() => _notifications = value);
                },
              ),
            ),
            CupertinoListTile(
              title: Text(l10n.darkModeLabel),
              trailing: CupertinoSwitch(
                value: _darkMode,
                onChanged: (value) {
                  setState(() => _darkMode = value);
                },
              ),
            ),
            CupertinoListTile(
              title: Text(l10n.analyticsLabel),
              subtitle: Text(l10n.analyticsDescription),
              trailing: CupertinoSwitch(
                value: _analytics,
                onChanged: (value) {
                  setState(() => _analytics = value);
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Advanced CupertinoSwitch Patterns for Localization

Settings Page with Grouped Toggles

iOS settings pages organize toggles into localized sections with headers and footers.

class LocalizedCupertinoSettings extends StatefulWidget {
  const LocalizedCupertinoSettings({super.key});

  @override
  State<LocalizedCupertinoSettings> createState() =>
      _LocalizedCupertinoSettingsState();
}

class _LocalizedCupertinoSettingsState
    extends State<LocalizedCupertinoSettings> {
  bool _pushNotifications = true;
  bool _emailNotifications = false;
  bool _soundEnabled = true;
  bool _vibration = true;
  bool _locationServices = false;
  bool _autoBackup = true;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.settingsTitle),
      ),
      child: SafeArea(
        child: ListView(
          children: [
            CupertinoListSection.insetGrouped(
              header: Text(l10n.notificationsSection),
              footer: Text(l10n.notificationsFooter),
              children: [
                CupertinoListTile(
                  title: Text(l10n.pushNotificationsLabel),
                  trailing: CupertinoSwitch(
                    value: _pushNotifications,
                    onChanged: (value) {
                      setState(() => _pushNotifications = value);
                    },
                  ),
                ),
                CupertinoListTile(
                  title: Text(l10n.emailNotificationsLabel),
                  trailing: CupertinoSwitch(
                    value: _emailNotifications,
                    onChanged: (value) {
                      setState(() => _emailNotifications = value);
                    },
                  ),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              header: Text(l10n.soundAndHapticsSection),
              children: [
                CupertinoListTile(
                  title: Text(l10n.soundLabel),
                  trailing: CupertinoSwitch(
                    value: _soundEnabled,
                    onChanged: (value) {
                      setState(() => _soundEnabled = value);
                    },
                  ),
                ),
                CupertinoListTile(
                  title: Text(l10n.vibrationLabel),
                  trailing: CupertinoSwitch(
                    value: _vibration,
                    onChanged: (value) {
                      setState(() => _vibration = value);
                    },
                  ),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              header: Text(l10n.privacySection),
              footer: Text(l10n.privacyFooter),
              children: [
                CupertinoListTile(
                  title: Text(l10n.locationServicesLabel),
                  subtitle: Text(l10n.locationServicesDescription),
                  trailing: CupertinoSwitch(
                    value: _locationServices,
                    onChanged: (value) {
                      setState(() => _locationServices = value);
                    },
                  ),
                ),
                CupertinoListTile(
                  title: Text(l10n.autoBackupLabel),
                  subtitle: Text(l10n.autoBackupDescription),
                  trailing: CupertinoSwitch(
                    value: _autoBackup,
                    onChanged: (value) {
                      setState(() => _autoBackup = value);
                    },
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Dependent Toggles with Localized State

Parent toggles enable or disable child toggles, with translated explanations for the disabled state.

class DependentCupertinoSwitches extends StatefulWidget {
  const DependentCupertinoSwitches({super.key});

  @override
  State<DependentCupertinoSwitches> createState() =>
      _DependentCupertinoSwitchesState();
}

class _DependentCupertinoSwitchesState
    extends State<DependentCupertinoSwitches> {
  bool _masterToggle = true;
  bool _option1 = true;
  bool _option2 = false;
  bool _option3 = true;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return CupertinoListSection.insetGrouped(
      header: Text(l10n.syncSettingsSection),
      footer: Text(
        _masterToggle
            ? l10n.syncEnabledFooter
            : l10n.syncDisabledFooter,
      ),
      children: [
        CupertinoListTile(
          title: Text(
            l10n.syncEnabledLabel,
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
          trailing: CupertinoSwitch(
            value: _masterToggle,
            onChanged: (value) {
              setState(() => _masterToggle = value);
            },
          ),
        ),
        CupertinoListTile(
          title: Text(l10n.syncContactsLabel),
          trailing: CupertinoSwitch(
            value: _option1 && _masterToggle,
            onChanged: _masterToggle
                ? (value) => setState(() => _option1 = value)
                : null,
          ),
        ),
        CupertinoListTile(
          title: Text(l10n.syncCalendarLabel),
          trailing: CupertinoSwitch(
            value: _option2 && _masterToggle,
            onChanged: _masterToggle
                ? (value) => setState(() => _option2 = value)
                : null,
          ),
        ),
        CupertinoListTile(
          title: Text(l10n.syncPhotosLabel),
          trailing: CupertinoSwitch(
            value: _option3 && _masterToggle,
            onChanged: _masterToggle
                ? (value) => setState(() => _option3 = value)
                : null,
          ),
        ),
      ],
    );
  }
}

Toggle with Confirmation Dialog

Toggles that require confirmation before enabling, showing a localized alert dialog.

class ConfirmationCupertinoSwitch extends StatefulWidget {
  const ConfirmationCupertinoSwitch({super.key});

  @override
  State<ConfirmationCupertinoSwitch> createState() =>
      _ConfirmationCupertinoSwitchState();
}

class _ConfirmationCupertinoSwitchState
    extends State<ConfirmationCupertinoSwitch> {
  bool _developerMode = false;

  Future<void> _toggleDeveloperMode(bool value) async {
    if (value) {
      final l10n = AppLocalizations.of(context)!;
      final confirmed = await showCupertinoDialog<bool>(
        context: context,
        builder: (context) {
          final dialogL10n = AppLocalizations.of(context)!;
          return CupertinoAlertDialog(
            title: Text(dialogL10n.enableDeveloperModeTitle),
            content: Text(dialogL10n.enableDeveloperModeMessage),
            actions: [
              CupertinoDialogAction(
                onPressed: () => Navigator.pop(context, false),
                child: Text(dialogL10n.cancelButton),
              ),
              CupertinoDialogAction(
                isDestructiveAction: true,
                onPressed: () => Navigator.pop(context, true),
                child: Text(dialogL10n.enableButton),
              ),
            ],
          );
        },
      );
      if (confirmed == true && mounted) {
        setState(() => _developerMode = true);
      }
    } else {
      setState(() => _developerMode = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return CupertinoListSection.insetGrouped(
      header: Text(l10n.advancedSection),
      footer: Text(
        _developerMode
            ? l10n.developerModeEnabledFooter
            : l10n.developerModeDisabledFooter,
      ),
      children: [
        CupertinoListTile(
          title: Text(l10n.developerModeLabel),
          subtitle: Text(l10n.developerModeDescription),
          trailing: CupertinoSwitch(
            value: _developerMode,
            onChanged: _toggleDeveloperMode,
          ),
        ),
      ],
    );
  }
}

RTL Support and Bidirectional Layouts

CupertinoSwitch maintains its visual appearance in RTL layouts. The toggle position remains consistent, while the label text and list tile layout adapt to the active directionality.

class BidirectionalCupertinoSwitch extends StatefulWidget {
  const BidirectionalCupertinoSwitch({super.key});

  @override
  State<BidirectionalCupertinoSwitch> createState() =>
      _BidirectionalCupertinoSwitchState();
}

class _BidirectionalCupertinoSwitchState
    extends State<BidirectionalCupertinoSwitch> {
  bool _enabled = true;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return CupertinoListSection.insetGrouped(
      children: [
        CupertinoListTile(
          title: Text(l10n.featureToggleLabel),
          subtitle: Text(
            _enabled ? l10n.enabledStatus : l10n.disabledStatus,
          ),
          trailing: CupertinoSwitch(
            value: _enabled,
            onChanged: (value) {
              setState(() => _enabled = value);
            },
          ),
        ),
      ],
    );
  }
}

Testing CupertinoSwitch 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 LocalizedCupertinoSwitchExample(),
    );
  }

  testWidgets('CupertinoSwitch renders with localized labels', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(CupertinoSwitch), findsWidgets);
  });

  testWidgets('CupertinoSwitch toggles state', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();

    await tester.tap(find.byType(CupertinoSwitch).first);
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });

  testWidgets('CupertinoSwitch works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Use CupertinoListSection.insetGrouped to wrap toggle settings in iOS-style grouped sections with translated headers and footers.

  2. Provide translated subtitle text on CupertinoListTile to explain what each toggle controls, especially for non-obvious settings.

  3. Use section footer text to show translated explanations that update based on toggle state (enabled/disabled descriptions).

  4. Disable dependent toggles by passing null to onChanged when the parent toggle is off, and explain the disabled state with translated footer text.

  5. Show confirmation dialogs for dangerous toggles (developer mode, data deletion) with translated warning messages before enabling.

  6. Test toggle accessibility in RTL to verify VoiceOver announces the correct translated label and on/off state.

Conclusion

CupertinoSwitch provides iOS-native toggle styling for Flutter settings screens. For multilingual apps, it pairs seamlessly with CupertinoListTile and CupertinoListSection to build settings interfaces with translated labels, section headers, explanatory footers, and dependent toggle relationships. By combining CupertinoSwitch with confirmation dialogs, state-dependent descriptions, and grouped sections, you can build iOS settings experiences that are clear and intuitive in every supported language.

Further Reading