Flutter Clipboard Localization: Copy, Paste, and Share Feedback Messages
Create seamless clipboard interactions in any language. This guide covers localizing copy/paste feedback, share sheets, clipboard previews, and data format handling in Flutter applications.
Clipboard Localization Challenges
Clipboard features require localization for:
- Action feedback - "Copied!", "Pasted successfully"
- Share sheet options - "Copy link", "Share via..."
- Error messages - "Nothing to paste", "Clipboard empty"
- Data format labels - "Plain text", "Rich text", "Image"
- Permission messages - Clipboard access explanations
Setting Up Clipboard Localization
ARB File Structure
{
"@@locale": "en",
"copyToClipboard": "Copy",
"@copyToClipboard": {
"description": "Copy action button label"
},
"copyLinkToClipboard": "Copy Link",
"@copyLinkToClipboard": {
"description": "Copy link action label"
},
"copyTextToClipboard": "Copy Text",
"@copyTextToClipboard": {
"description": "Copy text action label"
},
"copyImageToClipboard": "Copy Image",
"@copyImageToClipboard": {
"description": "Copy image action label"
},
"copyCodeToClipboard": "Copy Code",
"@copyCodeToClipboard": {
"description": "Copy code snippet action label"
},
"copiedToClipboard": "Copied to clipboard",
"@copiedToClipboard": {
"description": "Success message after copying"
},
"copiedItemToClipboard": "{item} copied to clipboard",
"@copiedItemToClipboard": {
"description": "Success message with item name",
"placeholders": {
"item": {
"type": "String",
"description": "Name of copied item"
}
}
},
"pasteFromClipboard": "Paste",
"@pasteFromClipboard": {
"description": "Paste action button label"
},
"pastedFromClipboard": "Pasted from clipboard",
"@pastedFromClipboard": {
"description": "Success message after pasting"
},
"clipboardEmpty": "Clipboard is empty",
"@clipboardEmpty": {
"description": "Message when clipboard has no content"
},
"clipboardAccessDenied": "Clipboard access denied",
"@clipboardAccessDenied": {
"description": "Error when clipboard access is blocked"
}
}
Share Actions Localization
{
"share": "Share",
"@share": {
"description": "Share action button label"
},
"shareVia": "Share via...",
"@shareVia": {
"description": "Share via menu label"
},
"shareLink": "Share Link",
"@shareLink": {
"description": "Share link option"
},
"shareText": "Share Text",
"@shareText": {
"description": "Share text option"
},
"shareImage": "Share Image",
"@shareImage": {
"description": "Share image option"
},
"shareFile": "Share File",
"@shareFile": {
"description": "Share file option"
},
"shareToApp": "Share to {appName}",
"@shareToApp": {
"description": "Share to specific app",
"placeholders": {
"appName": {
"type": "String"
}
}
},
"shareSubject": "Check this out",
"@shareSubject": {
"description": "Default share subject line"
},
"shareSuccess": "Shared successfully",
"@shareSuccess": {
"description": "Share success message"
},
"shareFailed": "Could not share. Please try again.",
"@shareFailed": {
"description": "Share failed error message"
},
"shareCancelled": "Share cancelled",
"@shareCancelled": {
"description": "Share cancelled message"
}
}
Implementing Localized Clipboard Service
Clipboard Service with Feedback
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum ClipboardContentType {
text,
link,
code,
image,
richText,
}
class LocalizedClipboardService {
/// Copy text to clipboard with localized feedback
static Future<void> copyText(
BuildContext context,
String text, {
ClipboardContentType type = ClipboardContentType.text,
String? itemName,
bool showFeedback = true,
}) async {
final l10n = AppLocalizations.of(context)!;
try {
await Clipboard.setData(ClipboardData(text: text));
if (showFeedback) {
final message = itemName != null
? l10n.copiedItemToClipboard(itemName)
: l10n.copiedToClipboard;
_showFeedback(context, message, success: true);
}
} catch (e) {
if (showFeedback) {
_showFeedback(context, l10n.clipboardCopyFailed, success: false);
}
}
}
/// Copy with custom success message based on content type
static Future<void> copyWithType(
BuildContext context,
String text,
ClipboardContentType type,
) async {
final l10n = AppLocalizations.of(context)!;
try {
await Clipboard.setData(ClipboardData(text: text));
final message = _getSuccessMessage(l10n, type);
_showFeedback(context, message, success: true);
} catch (e) {
_showFeedback(context, l10n.clipboardCopyFailed, success: false);
}
}
static String _getSuccessMessage(AppLocalizations l10n, ClipboardContentType type) {
switch (type) {
case ClipboardContentType.link:
return l10n.linkCopied;
case ClipboardContentType.code:
return l10n.codeCopied;
case ClipboardContentType.image:
return l10n.imageCopied;
default:
return l10n.copiedToClipboard;
}
}
/// Paste from clipboard
static Future<String?> paste(
BuildContext context, {
bool showFeedback = true,
}) async {
final l10n = AppLocalizations.of(context)!;
try {
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text == null || data!.text!.isEmpty) {
if (showFeedback) {
_showFeedback(context, l10n.clipboardEmpty, success: false);
}
return null;
}
if (showFeedback) {
_showFeedback(context, l10n.pastedFromClipboard, success: true);
}
return data.text;
} catch (e) {
if (showFeedback) {
_showFeedback(context, l10n.clipboardAccessDenied, success: false);
}
return null;
}
}
/// Check if clipboard has content
static Future<bool> hasContent() async {
try {
final data = await Clipboard.getData(Clipboard.kTextPlain);
return data?.text?.isNotEmpty ?? false;
} catch (e) {
return false;
}
}
static void _showFeedback(
BuildContext context,
String message, {
required bool success,
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
success ? Icons.check_circle : Icons.error,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(message),
],
),
backgroundColor: success ? Colors.green : Colors.red,
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
}
Copy Button Widget
Reusable Copy Button with Localization
class LocalizedCopyButton extends StatefulWidget {
final String textToCopy;
final ClipboardContentType contentType;
final String? itemName;
final Widget? icon;
final Widget? copiedIcon;
final String? tooltip;
final ButtonStyle? style;
final bool showLabel;
const LocalizedCopyButton({
Key? key,
required this.textToCopy,
this.contentType = ClipboardContentType.text,
this.itemName,
this.icon,
this.copiedIcon,
this.tooltip,
this.style,
this.showLabel = false,
}) : super(key: key);
@override
State<LocalizedCopyButton> createState() => _LocalizedCopyButtonState();
}
class _LocalizedCopyButtonState extends State<LocalizedCopyButton> {
bool _copied = false;
Future<void> _handleCopy() async {
await LocalizedClipboardService.copyText(
context,
widget.textToCopy,
type: widget.contentType,
itemName: widget.itemName,
);
setState(() => _copied = true);
// Reset after 2 seconds
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() => _copied = false);
}
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final defaultIcon = Icon(
_copied ? Icons.check : Icons.copy,
size: 20,
);
final currentIcon = _copied
? (widget.copiedIcon ?? defaultIcon)
: (widget.icon ?? defaultIcon);
final label = _getLabel(l10n);
final tooltipText = widget.tooltip ?? label;
if (widget.showLabel) {
return TextButton.icon(
onPressed: _handleCopy,
icon: currentIcon,
label: Text(_copied ? l10n.copied : label),
style: widget.style,
);
}
return IconButton(
onPressed: _handleCopy,
icon: currentIcon,
tooltip: tooltipText,
style: widget.style,
);
}
String _getLabel(AppLocalizations l10n) {
switch (widget.contentType) {
case ClipboardContentType.link:
return l10n.copyLinkToClipboard;
case ClipboardContentType.code:
return l10n.copyCodeToClipboard;
case ClipboardContentType.image:
return l10n.copyImageToClipboard;
default:
return l10n.copyToClipboard;
}
}
}
Code Block with Copy Button
class LocalizedCodeBlock extends StatelessWidget {
final String code;
final String? language;
final bool showLineNumbers;
const LocalizedCodeBlock({
Key? key,
required this.code,
this.language,
this.showLineNumbers = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header with language and copy button
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[850],
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
),
child: Row(
children: [
if (language != null) ...[
Text(
language!,
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
],
LocalizedCopyButton(
textToCopy: code,
contentType: ClipboardContentType.code,
showLabel: true,
),
],
),
),
// Code content
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(12),
child: SelectableText(
code,
style: const TextStyle(
fontFamily: 'monospace',
color: Colors.white,
fontSize: 14,
),
),
),
],
),
);
}
}
Share Sheet Implementation
Localized Share Service
import 'package:share_plus/share_plus.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedShareService {
/// Share text with localized subject
static Future<void> shareText(
BuildContext context,
String text, {
String? subject,
}) async {
final l10n = AppLocalizations.of(context)!;
try {
final result = await Share.share(
text,
subject: subject ?? l10n.shareSubject,
);
_handleShareResult(context, result, l10n);
} catch (e) {
_showError(context, l10n.shareFailed);
}
}
/// Share link with preview text
static Future<void> shareLink(
BuildContext context,
String url, {
String? title,
String? description,
}) async {
final l10n = AppLocalizations.of(context)!;
try {
final shareText = _buildShareText(url, title, description);
final result = await Share.share(
shareText,
subject: title ?? l10n.shareSubject,
);
_handleShareResult(context, result, l10n);
} catch (e) {
_showError(context, l10n.shareFailed);
}
}
/// Share file with localized messages
static Future<void> shareFile(
BuildContext context,
String filePath, {
String? mimeType,
String? subject,
}) async {
final l10n = AppLocalizations.of(context)!;
try {
final result = await Share.shareXFiles(
[XFile(filePath, mimeType: mimeType)],
subject: subject ?? l10n.shareSubject,
);
_handleShareResult(context, result, l10n);
} catch (e) {
_showError(context, l10n.shareFailed);
}
}
/// Share multiple files
static Future<void> shareFiles(
BuildContext context,
List<String> filePaths, {
String? subject,
String? text,
}) async {
final l10n = AppLocalizations.of(context)!;
try {
final files = filePaths.map((path) => XFile(path)).toList();
final result = await Share.shareXFiles(
files,
subject: subject ?? l10n.shareSubject,
text: text,
);
_handleShareResult(context, result, l10n);
} catch (e) {
_showError(context, l10n.shareFailed);
}
}
static String _buildShareText(String url, String? title, String? description) {
final buffer = StringBuffer();
if (title != null) {
buffer.writeln(title);
}
if (description != null) {
buffer.writeln(description);
}
buffer.write(url);
return buffer.toString();
}
static void _handleShareResult(
BuildContext context,
ShareResult result,
AppLocalizations l10n,
) {
switch (result.status) {
case ShareResultStatus.success:
// Optionally show success message
break;
case ShareResultStatus.dismissed:
// User cancelled - no message needed
break;
case ShareResultStatus.unavailable:
_showError(context, l10n.shareUnavailable);
break;
}
}
static void _showError(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
}
Share Bottom Sheet
class LocalizedShareBottomSheet extends StatelessWidget {
final String content;
final String? url;
final String? title;
final ShareContentType contentType;
const LocalizedShareBottomSheet({
Key? key,
required this.content,
this.url,
this.title,
this.contentType = ShareContentType.text,
}) : super(key: key);
static Future<void> show(
BuildContext context, {
required String content,
String? url,
String? title,
ShareContentType contentType = ShareContentType.text,
}) {
return showModalBottomSheet(
context: context,
builder: (context) => LocalizedShareBottomSheet(
content: content,
url: url,
title: title,
contentType: contentType,
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text(
l10n.shareVia,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(),
// Share options
ListTile(
leading: const Icon(Icons.copy),
title: Text(l10n.copyToClipboard),
onTap: () {
LocalizedClipboardService.copyText(context, content);
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.share),
title: Text(l10n.share),
subtitle: Text(l10n.shareToOtherApps),
onTap: () {
Navigator.pop(context);
LocalizedShareService.shareText(context, content);
},
),
if (url != null) ...[
ListTile(
leading: const Icon(Icons.link),
title: Text(l10n.copyLinkToClipboard),
onTap: () {
LocalizedClipboardService.copyText(
context,
url!,
type: ClipboardContentType.link,
);
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.open_in_browser),
title: Text(l10n.shareLink),
onTap: () {
Navigator.pop(context);
LocalizedShareService.shareLink(context, url!, title: title);
},
),
],
],
),
),
);
}
}
enum ShareContentType {
text,
link,
image,
file,
}
Clipboard Preview Widget
Show Clipboard Contents
class LocalizedClipboardPreview extends StatefulWidget {
final VoidCallback? onPaste;
final bool showPreview;
const LocalizedClipboardPreview({
Key? key,
this.onPaste,
this.showPreview = true,
}) : super(key: key);
@override
State<LocalizedClipboardPreview> createState() => _LocalizedClipboardPreviewState();
}
class _LocalizedClipboardPreviewState extends State<LocalizedClipboardPreview> {
String? _clipboardContent;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadClipboardContent();
}
Future<void> _loadClipboardContent() async {
try {
final data = await Clipboard.getData(Clipboard.kTextPlain);
setState(() {
_clipboardContent = data?.text;
_isLoading = false;
});
} catch (e) {
setState(() {
_clipboardContent = null;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_clipboardContent == null || _clipboardContent!.isEmpty) {
return _buildEmptyState(l10n);
}
return _buildPreview(l10n);
}
Widget _buildEmptyState(AppLocalizations l10n) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Row(
children: [
Icon(Icons.content_paste_off, color: Colors.grey[400]),
const SizedBox(width: 12),
Text(
l10n.clipboardEmpty,
style: TextStyle(color: Colors.grey[600]),
),
],
),
);
}
Widget _buildPreview(AppLocalizations l10n) {
final preview = _clipboardContent!.length > 100
? '${_clipboardContent!.substring(0, 100)}...'
: _clipboardContent!;
final isUrl = Uri.tryParse(_clipboardContent!)?.hasScheme ?? false;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isUrl ? Icons.link : Icons.content_paste,
color: Colors.blue[700],
size: 20,
),
const SizedBox(width: 8),
Text(
isUrl ? l10n.clipboardContainsLink : l10n.clipboardContainsText,
style: TextStyle(
color: Colors.blue[700],
fontWeight: FontWeight.w500,
),
),
const Spacer(),
if (widget.onPaste != null)
TextButton.icon(
onPressed: widget.onPaste,
icon: const Icon(Icons.paste, size: 18),
label: Text(l10n.pasteFromClipboard),
),
],
),
if (widget.showPreview) ...[
const SizedBox(height: 8),
Text(
preview,
style: TextStyle(
color: Colors.grey[700],
fontSize: 13,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
],
),
);
}
}
Context Menu Integration
Localized Selection Controls
class LocalizedSelectionControls extends MaterialTextSelectionControls {
final BuildContext context;
LocalizedSelectionControls(this.context);
@override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset selectionMidpoint,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ClipboardStatusNotifier? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
final l10n = AppLocalizations.of(this.context)!;
return _LocalizedTextSelectionToolbar(
l10n: l10n,
anchorAbove: selectionMidpoint,
anchorBelow: selectionMidpoint,
clipboardStatus: clipboardStatus,
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
);
}
}
class _LocalizedTextSelectionToolbar extends StatelessWidget {
final AppLocalizations l10n;
final Offset anchorAbove;
final Offset anchorBelow;
final ClipboardStatusNotifier? clipboardStatus;
final VoidCallback? handleCut;
final VoidCallback? handleCopy;
final VoidCallback? handlePaste;
final VoidCallback? handleSelectAll;
const _LocalizedTextSelectionToolbar({
required this.l10n,
required this.anchorAbove,
required this.anchorBelow,
required this.clipboardStatus,
this.handleCut,
this.handleCopy,
this.handlePaste,
this.handleSelectAll,
});
@override
Widget build(BuildContext context) {
final items = <Widget>[];
if (handleCut != null) {
items.add(_buildButton(l10n.cut, Icons.cut, handleCut!));
}
if (handleCopy != null) {
items.add(_buildButton(l10n.copyToClipboard, Icons.copy, handleCopy!));
}
if (handlePaste != null) {
items.add(_buildButton(l10n.pasteFromClipboard, Icons.paste, handlePaste!));
}
if (handleSelectAll != null) {
items.add(_buildButton(l10n.selectAll, Icons.select_all, handleSelectAll!));
}
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
children: items,
);
}
Widget _buildButton(String label, IconData icon, VoidCallback onPressed) {
return TextSelectionToolbarTextButton(
padding: const EdgeInsets.symmetric(horizontal: 12),
onPressed: onPressed,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 4),
Text(label),
],
),
);
}
}
Additional ARB Entries
{
"cut": "Cut",
"@cut": {
"description": "Cut action label"
},
"selectAll": "Select All",
"@selectAll": {
"description": "Select all action label"
},
"copied": "Copied!",
"@copied": {
"description": "Short copy confirmation"
},
"linkCopied": "Link copied",
"@linkCopied": {
"description": "Link copy confirmation"
},
"codeCopied": "Code copied",
"@codeCopied": {
"description": "Code copy confirmation"
},
"imageCopied": "Image copied",
"@imageCopied": {
"description": "Image copy confirmation"
},
"clipboardCopyFailed": "Failed to copy to clipboard",
"@clipboardCopyFailed": {
"description": "Copy failure message"
},
"clipboardContainsText": "Clipboard contains text",
"@clipboardContainsText": {
"description": "Clipboard preview label for text"
},
"clipboardContainsLink": "Clipboard contains a link",
"@clipboardContainsLink": {
"description": "Clipboard preview label for links"
},
"shareToOtherApps": "Send to other apps",
"@shareToOtherApps": {
"description": "Share to other apps subtitle"
},
"shareUnavailable": "Sharing is not available on this device",
"@shareUnavailable": {
"description": "Share unavailable error"
}
}
Testing Clipboard Localization
Unit Tests
void main() {
group('LocalizedClipboardService', () {
testWidgets('shows localized copy success message', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
locale: const Locale('es'),
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => LocalizedClipboardService.copyText(
context,
'Test text',
),
child: const Text('Copy'),
),
),
),
);
await tester.tap(find.text('Copy'));
await tester.pumpAndSettle();
expect(find.text('Copiado al portapapeles'), findsOneWidget);
});
testWidgets('shows localized empty clipboard message', (tester) async {
// Test implementation for empty clipboard state
});
});
}
Best Practices
- Provide immediate feedback - Users need confirmation that copy worked
- Use appropriate icons - Check mark after copying, clipboard icon for paste
- Handle errors gracefully - Some platforms restrict clipboard access
- Support keyboard shortcuts - Ctrl+C/Cmd+C should show localized feedback
- Consider accessibility - Screen readers should announce clipboard actions
- Test on all platforms - Web has different clipboard permissions than mobile
Conclusion
Localized clipboard and sharing features enhance user experience by providing clear, understandable feedback in every language. By implementing proper localization for copy/paste actions, share sheets, and error messages, you create a polished application that feels native to users worldwide.
Remember to test clipboard functionality across all platforms, as permissions and behaviors vary between iOS, Android, and web environments.