Flutter CupertinoAlertDialog Localization: iOS-Style Alerts for Multilingual Apps
CupertinoAlertDialog is a Flutter widget that displays an iOS-style alert dialog with a title, content, and action buttons. In multilingual applications, CupertinoAlertDialog is essential for presenting translated confirmation prompts, showing localized error and warning messages, providing destructive action confirmations in the active language, and maintaining iOS dialog conventions across all supported locales.
Understanding CupertinoAlertDialog in Localization Context
CupertinoAlertDialog renders a rounded-rectangle dialog centered on screen with a blurred background, a title, optional content text, and horizontally or vertically arranged action buttons. For multilingual apps, this enables:
- Translated dialog titles and content messages
- Localized action button labels with destructive/default styling
- Parameterized messages with dynamic values in the active language
- Accessible dialog announcements for VoiceOver in the correct locale
Why CupertinoAlertDialog Matters for Multilingual Apps
CupertinoAlertDialog provides:
- iOS-native appearance: Rounded dialog with blurred backdrop matching iOS conventions
- Action styling: Destructive (red) and default (bold) actions with translated labels
- Layout adaptation: Buttons stack vertically when translated labels are too long for horizontal layout
- Accessibility: Dialog title and content announced by VoiceOver in the active language
Basic CupertinoAlertDialog Implementation
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedCupertinoAlertExample extends StatelessWidget {
const LocalizedCupertinoAlertExample({super.key});
void _showSimpleAlert(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
showCupertinoDialog(
context: context,
builder: (context) {
final dialogL10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(dialogL10n.welcomeTitle),
content: Text(dialogL10n.welcomeMessage),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context),
child: Text(dialogL10n.okButton),
),
],
);
},
);
}
void _showConfirmation(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
showCupertinoDialog(
context: context,
builder: (context) {
final dialogL10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(dialogL10n.deleteItemTitle),
content: Text(dialogL10n.deleteItemMessage),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: Text(dialogL10n.cancelButton),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, true),
child: Text(dialogL10n.deleteButton),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.alertsTitle),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoButton.filled(
onPressed: () => _showSimpleAlert(context),
child: Text(l10n.showAlertButton),
),
const SizedBox(height: 16),
CupertinoButton(
color: CupertinoColors.destructiveRed,
onPressed: () => _showConfirmation(context),
child: Text(l10n.deleteItemButton),
),
],
),
),
),
);
}
}
Advanced CupertinoAlertDialog Patterns for Localization
Alert with Parameterized Message
Dynamic values like item names, counts, and dates should be inserted into translated dialog messages using ARB placeholders.
class ParameterizedAlertDialog extends StatelessWidget {
const ParameterizedAlertDialog({super.key});
void _showDeleteConfirmation(BuildContext context, String itemName) {
showCupertinoDialog(
context: context,
builder: (context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(l10n.deleteConfirmTitle),
content: Text(l10n.deleteConfirmMessage(itemName)),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.deleteButton),
),
],
);
},
);
}
void _showItemCountAlert(BuildContext context, int count) {
showCupertinoDialog(
context: context,
builder: (context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(l10n.removeItemsTitle),
content: Text(l10n.removeItemsMessage(count)),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.removeAllButton),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoButton(
onPressed: () => _showDeleteConfirmation(context, 'Document.pdf'),
child: Text(l10n.deleteFileButton),
),
CupertinoButton(
onPressed: () => _showItemCountAlert(context, 5),
child: Text(l10n.clearCartButton),
),
],
),
);
}
}
Alert Dialog with Text Input
Alert dialogs that collect user input with a translated placeholder and validation message.
class AlertWithTextInput extends StatefulWidget {
const AlertWithTextInput({super.key});
@override
State<AlertWithTextInput> createState() => _AlertWithTextInputState();
}
class _AlertWithTextInputState extends State<AlertWithTextInput> {
void _showRenameDialog(String currentName) {
final controller = TextEditingController(text: currentName);
showCupertinoDialog(
context: context,
builder: (context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(l10n.renameTitle),
content: Padding(
padding: const EdgeInsets.only(top: 12),
child: CupertinoTextField(
controller: controller,
placeholder: l10n.newNamePlaceholder,
autofocus: true,
clearButtonMode: OverlayVisibilityMode.editing,
),
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
final newName = controller.text.trim();
if (newName.isNotEmpty) {
Navigator.pop(context, newName);
}
},
child: Text(l10n.renameButton),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: CupertinoButton(
onPressed: () => _showRenameDialog('My Document'),
child: Text(l10n.renameFileButton),
),
);
}
}
Multi-Option Alert Dialog
Alerts with more than two options stack vertically, each with a translated label.
class MultiOptionAlert extends StatelessWidget {
const MultiOptionAlert({super.key});
void _showSaveOptions(BuildContext context) {
showCupertinoDialog(
context: context,
builder: (context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(l10n.unsavedChangesTitle),
content: Text(l10n.unsavedChangesMessage),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context, 'save'),
child: Text(l10n.saveChangesButton),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, 'discard'),
child: Text(l10n.discardChangesButton),
),
CupertinoDialogAction(
onPressed: () => Navigator.pop(context, 'cancel'),
child: Text(l10n.cancelButton),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: CupertinoButton.filled(
onPressed: () => _showSaveOptions(context),
child: Text(l10n.closeDocumentButton),
),
);
}
}
Error Alert with Localized Details
Error dialogs show translated error titles, descriptions, and recovery suggestions.
class LocalizedErrorAlert extends StatelessWidget {
const LocalizedErrorAlert({super.key});
void _showError(BuildContext context, String errorCode) {
showCupertinoDialog(
context: context,
builder: (context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(l10n.errorTitle),
content: Column(
children: [
const SizedBox(height: 8),
Text(l10n.networkErrorMessage),
const SizedBox(height: 8),
Text(
l10n.errorCodeLabel(errorCode),
style: const TextStyle(
fontSize: 12,
color: CupertinoColors.systemGrey,
),
),
],
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context, 'retry'),
child: Text(l10n.retryButton),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context),
child: Text(l10n.okButton),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: CupertinoButton(
onPressed: () => _showError(context, 'ERR_TIMEOUT'),
child: Text(l10n.simulateErrorButton),
),
);
}
}
RTL Support and Bidirectional Layouts
CupertinoAlertDialog content and actions automatically adapt to RTL. Text aligns correctly, and action button order follows the platform convention.
class BidirectionalCupertinoAlert extends StatelessWidget {
const BidirectionalCupertinoAlert({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: CupertinoButton.filled(
onPressed: () {
showCupertinoDialog(
context: context,
builder: (context) {
final dialogL10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(dialogL10n.confirmTitle),
content: Text(dialogL10n.confirmMessage),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: Text(dialogL10n.cancelButton),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context, true),
child: Text(dialogL10n.confirmButton),
),
],
);
},
);
},
child: Text(l10n.showAlertButton),
),
);
}
}
Testing CupertinoAlertDialog 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 LocalizedCupertinoAlertExample(),
);
}
testWidgets('CupertinoAlertDialog shows localized content', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
await tester.tap(find.byType(CupertinoButton).first);
await tester.pumpAndSettle();
expect(find.byType(CupertinoAlertDialog), findsOneWidget);
});
testWidgets('CupertinoAlertDialog works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
Best Practices
Use
isDestructiveAction: truefor delete/remove actions so the button label renders in red, a universally understood iOS convention.Use
isDefaultAction: truefor the primary action so it renders in bold, guiding users to the expected action regardless of language.Use parameterized ARB messages for dialog content that includes dynamic values like item names, counts, or error codes.
Keep dialog titles short -- iOS dialogs center the title, so long translated titles may wrap awkwardly. Move details to the content area.
Use
CupertinoTextFieldinside dialog content for input dialogs, matching the iOS pattern of inline text fields within alerts.Test with 3+ actions to verify that buttons stack vertically correctly and translated labels don't overflow.
Conclusion
CupertinoAlertDialog is the standard iOS alert for Flutter apps, providing a familiar rounded-rectangle dialog with blurred backdrop. For multilingual apps, it handles translated titles, parameterized content messages, and localized action buttons while automatically adapting button layout when translated labels are too long for horizontal arrangement. By combining alert dialogs with destructive/default styling, text input, and error details, you can build iOS-native dialog experiences that communicate clearly in every supported language.