← Back to Blog

Flutter CupertinoContextMenu Localization: iOS Context Menus for Multilingual Apps

fluttercupertinocontextmenuioslocalizationrtl

Flutter CupertinoContextMenu Localization: iOS Context Menus for Multilingual Apps

CupertinoContextMenu is a Flutter widget that renders an iOS-style context menu triggered by a long press, displaying a preview of the content alongside a list of action items. In multilingual applications, CupertinoContextMenu is essential for displaying translated action labels that match iOS platform conventions, providing localized destructive action confirmations, supporting RTL text alignment within menu items for Arabic and Hebrew, and building accessible context menus with announcements in the active language.

Understanding CupertinoContextMenu in Localization Context

CupertinoContextMenu shows a full-screen preview with action buttons when the user long-presses a widget. For multilingual apps, this enables:

  • Translated action labels (Share, Copy, Delete) in the context menu
  • Localized destructive action text with proper warning semantics
  • RTL-aligned menu item text and icons
  • Accessible action descriptions in the active language

Why CupertinoContextMenu Matters for Multilingual Apps

CupertinoContextMenu provides:

  • iOS consistency: Context menus matching native iOS long-press interactions in every language
  • Action clarity: Clearly translated action labels so users understand each option
  • Destructive warnings: Properly localized destructive actions (Delete, Remove) with red styling
  • Platform feel: iOS users expect Cupertino-style context menus regardless of language

Basic CupertinoContextMenu Implementation

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

class LocalizedCupertinoContextMenuExample extends StatelessWidget {
  const LocalizedCupertinoContextMenuExample({super.key});

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.contextMenuTitle),
      ),
      child: Center(
        child: CupertinoContextMenu(
          actions: [
            CupertinoContextMenuAction(
              onPressed: () => Navigator.pop(context),
              trailingIcon: CupertinoIcons.share,
              child: Text(l10n.shareAction),
            ),
            CupertinoContextMenuAction(
              onPressed: () => Navigator.pop(context),
              trailingIcon: CupertinoIcons.doc_on_doc,
              child: Text(l10n.copyAction),
            ),
            CupertinoContextMenuAction(
              onPressed: () => Navigator.pop(context),
              trailingIcon: CupertinoIcons.bookmark,
              child: Text(l10n.saveAction),
            ),
            CupertinoContextMenuAction(
              onPressed: () => Navigator.pop(context),
              isDestructiveAction: true,
              trailingIcon: CupertinoIcons.delete,
              child: Text(l10n.deleteAction),
            ),
          ],
          child: Container(
            width: 200,
            height: 200,
            decoration: BoxDecoration(
              color: CupertinoColors.systemBlue,
              borderRadius: BorderRadius.circular(16),
            ),
            child: Center(
              child: Text(
                l10n.longPressHint,
                style: const TextStyle(
                  color: CupertinoColors.white,
                  fontSize: 16,
                ),
                textAlign: TextAlign.center,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Advanced CupertinoContextMenu Patterns for Localization

Photo Gallery Context Menu

CupertinoContextMenu for a photo gallery with localized image actions.

class PhotoContextMenuExample extends StatelessWidget {
  final String imageUrl;
  final String photoTitle;

  const PhotoContextMenuExample({
    super.key,
    required this.imageUrl,
    required this.photoTitle,
  });

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

    return CupertinoContextMenu(
      actions: [
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Share photo
          },
          trailingIcon: CupertinoIcons.share,
          child: Text(l10n.sharePhotoAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Save to album
          },
          trailingIcon: CupertinoIcons.photo_on_rectangle,
          child: Text(l10n.saveToAlbumAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Copy photo
          },
          trailingIcon: CupertinoIcons.doc_on_doc,
          child: Text(l10n.copyPhotoAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Add to favorites
          },
          trailingIcon: CupertinoIcons.heart,
          child: Text(l10n.addToFavoritesAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Delete photo
          },
          isDestructiveAction: true,
          trailingIcon: CupertinoIcons.delete,
          child: Text(l10n.deletePhotoAction),
        ),
      ],
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12),
        child: Image.network(
          imageUrl,
          width: 180,
          height: 180,
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}

Message Context Menu

CupertinoContextMenu for a messaging app with translated message actions.

class MessageContextMenuExample extends StatelessWidget {
  final String messageText;
  final bool isOwnMessage;

  const MessageContextMenuExample({
    super.key,
    required this.messageText,
    required this.isOwnMessage,
  });

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

    return CupertinoContextMenu(
      actions: [
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Reply to message
          },
          trailingIcon: CupertinoIcons.reply,
          child: Text(l10n.replyAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Copy message text
          },
          trailingIcon: CupertinoIcons.doc_on_doc,
          child: Text(l10n.copyTextAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Forward message
          },
          trailingIcon: CupertinoIcons.arrow_right,
          child: Text(l10n.forwardAction),
        ),
        if (isOwnMessage)
          CupertinoContextMenuAction(
            onPressed: () {
              Navigator.pop(context);
              // Edit message
            },
            trailingIcon: CupertinoIcons.pencil,
            child: Text(l10n.editMessageAction),
          ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Delete message
          },
          isDestructiveAction: true,
          trailingIcon: CupertinoIcons.delete,
          child: Text(
            isOwnMessage
                ? l10n.deleteForEveryoneAction
                : l10n.deleteForMeAction,
          ),
        ),
      ],
      child: Container(
        constraints: const BoxConstraints(maxWidth: 280),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
        decoration: BoxDecoration(
          color: isOwnMessage
              ? CupertinoColors.activeBlue
              : CupertinoColors.systemGrey5,
          borderRadius: BorderRadius.circular(18),
        ),
        child: Text(
          messageText,
          style: TextStyle(
            color: isOwnMessage ? CupertinoColors.white : CupertinoColors.black,
          ),
        ),
      ),
    );
  }
}

Link Preview Context Menu

CupertinoContextMenu for a link card with localized link actions.

class LinkContextMenuExample extends StatelessWidget {
  final String url;
  final String title;
  final String description;

  const LinkContextMenuExample({
    super.key,
    required this.url,
    required this.title,
    required this.description,
  });

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

    return CupertinoContextMenu(
      actions: [
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Open in browser
          },
          trailingIcon: CupertinoIcons.globe,
          child: Text(l10n.openInBrowserAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Copy link
          },
          trailingIcon: CupertinoIcons.link,
          child: Text(l10n.copyLinkAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Share link
          },
          trailingIcon: CupertinoIcons.share,
          child: Text(l10n.shareLinkAction),
        ),
        CupertinoContextMenuAction(
          onPressed: () {
            Navigator.pop(context);
            // Add to reading list
          },
          trailingIcon: CupertinoIcons.book,
          child: Text(l10n.addToReadingListAction),
        ),
      ],
      child: Container(
        width: 300,
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: CupertinoColors.systemBackground,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(
            color: CupertinoColors.systemGrey4,
          ),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              title,
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 16,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              description,
              style: TextStyle(
                fontSize: 14,
                color: CupertinoColors.systemGrey,
              ),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
            const SizedBox(height: 8),
            Text(
              url,
              style: TextStyle(
                fontSize: 12,
                color: CupertinoColors.activeBlue,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

CupertinoContextMenu action items display text aligned according to the active text direction. Trailing icons appear on the appropriate side for RTL languages. The surrounding content should use directional properties.

class BidirectionalContextMenuExample extends StatelessWidget {
  const BidirectionalContextMenuExample({super.key});

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.contextMenuTitle),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsetsDirectional.all(16),
          child: Align(
            alignment: AlignmentDirectional.centerStart,
            child: CupertinoContextMenu(
              actions: [
                CupertinoContextMenuAction(
                  onPressed: () => Navigator.pop(context),
                  trailingIcon: CupertinoIcons.share,
                  child: Text(l10n.shareAction),
                ),
                CupertinoContextMenuAction(
                  onPressed: () => Navigator.pop(context),
                  trailingIcon: CupertinoIcons.doc_on_doc,
                  child: Text(l10n.copyAction),
                ),
                CupertinoContextMenuAction(
                  onPressed: () => Navigator.pop(context),
                  isDestructiveAction: true,
                  trailingIcon: CupertinoIcons.delete,
                  child: Text(l10n.deleteAction),
                ),
              ],
              child: Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                  color: CupertinoColors.systemIndigo,
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Center(
                  child: Text(
                    l10n.longPressHint,
                    style: const TextStyle(
                      color: CupertinoColors.white,
                      fontSize: 16,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Testing CupertinoContextMenu 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 LocalizedCupertinoContextMenuExample(),
    );
  }

  testWidgets('CupertinoContextMenu renders correctly', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(CupertinoContextMenu), findsOneWidget);
  });

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

  testWidgets('CupertinoContextMenu renders in Spanish', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('es')));
    await tester.pumpAndSettle();
    expect(find.byType(CupertinoContextMenu), findsOneWidget);
  });
}

Best Practices

  1. Keep action labels concise — iOS context menu actions should be 1-3 words, so use short translated labels that clearly describe each action.

  2. Mark destructive actions — Set isDestructiveAction: true for delete, remove, and other irreversible actions so they render in red regardless of the active language.

  3. Use trailing icons — Pair each translated label with a recognizable icon via trailingIcon to reinforce meaning across languages.

  4. Conditionally show actions — Use conditions like isOwnMessage to show or hide context-specific actions, keeping translated labels relevant to the current state.

  5. Always call Navigator.pop — Every action's onPressed must dismiss the context menu by calling Navigator.pop(context) before performing the action.

  6. Test long translations — German and Finnish action labels can be significantly longer than English, so verify the context menu layout handles longer text without clipping.

Conclusion

CupertinoContextMenu provides iOS-style long-press context menus for Flutter apps. For multilingual apps, it handles translated action labels, supports destructive action styling, and aligns text correctly for RTL languages. By keeping labels concise, marking destructive actions, pairing text with icons, and testing across multiple locales, you can build context menus that feel native to iOS users in every supported language.

Further Reading