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
Use
CupertinoListSection.insetGroupedto wrap toggle settings in iOS-style grouped sections with translated headers and footers.Provide translated
subtitletext on CupertinoListTile to explain what each toggle controls, especially for non-obvious settings.Use section
footertext to show translated explanations that update based on toggle state (enabled/disabled descriptions).Disable dependent toggles by passing
nulltoonChangedwhen the parent toggle is off, and explain the disabled state with translated footer text.Show confirmation dialogs for dangerous toggles (developer mode, data deletion) with translated warning messages before enabling.
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.