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
Keep action labels concise — iOS context menu actions should be 1-3 words, so use short translated labels that clearly describe each action.
Mark destructive actions — Set
isDestructiveAction: truefor delete, remove, and other irreversible actions so they render in red regardless of the active language.Use trailing icons — Pair each translated label with a recognizable icon via
trailingIconto reinforce meaning across languages.Conditionally show actions — Use conditions like
isOwnMessageto show or hide context-specific actions, keeping translated labels relevant to the current state.Always call
Navigator.pop— Every action'sonPressedmust dismiss the context menu by callingNavigator.pop(context)before performing the action.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.