← Back to Blog

Flutter CupertinoAlertDialog Localization: iOS-Style Alerts for Multilingual Apps

fluttercupertinodialogioslocalizationrtl

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

  1. Use isDestructiveAction: true for delete/remove actions so the button label renders in red, a universally understood iOS convention.

  2. Use isDefaultAction: true for the primary action so it renders in bold, guiding users to the expected action regardless of language.

  3. Use parameterized ARB messages for dialog content that includes dynamic values like item names, counts, or error codes.

  4. Keep dialog titles short -- iOS dialogs center the title, so long translated titles may wrap awkwardly. Move details to the content area.

  5. Use CupertinoTextField inside dialog content for input dialogs, matching the iOS pattern of inline text fields within alerts.

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

Further Reading