Flutter Error Messages Localization: User-Friendly Multilingual Error Handling
Transform cryptic errors into helpful guidance in any language. This guide covers localizing error messages, crash screens, validation feedback, and troubleshooting tips in Flutter.
Error Localization Challenges
Error handling requires localization for:
- User-facing errors - Clear, actionable messages
- Validation errors - Form field feedback
- Network errors - Connection issues
- Crash screens - Graceful failure states
- Recovery actions - Retry, contact support
Error Message Architecture
Centralized Error Handler
class LocalizedErrorHandler {
final AppLocalizations l10n;
LocalizedErrorHandler(this.l10n);
String getMessage(AppError error) {
switch (error.type) {
case ErrorType.network:
return _getNetworkErrorMessage(error);
case ErrorType.authentication:
return _getAuthErrorMessage(error);
case ErrorType.validation:
return _getValidationErrorMessage(error);
case ErrorType.permission:
return _getPermissionErrorMessage(error);
case ErrorType.storage:
return _getStorageErrorMessage(error);
case ErrorType.server:
return _getServerErrorMessage(error);
case ErrorType.unknown:
default:
return l10n.errorUnknown;
}
}
String _getNetworkErrorMessage(AppError error) {
switch (error.code) {
case 'no_connection':
return l10n.errorNoConnection;
case 'timeout':
return l10n.errorTimeout;
case 'dns_lookup_failed':
return l10n.errorDnsLookup;
case 'ssl_error':
return l10n.errorSslCertificate;
case 'connection_refused':
return l10n.errorConnectionRefused;
default:
return l10n.errorNetworkGeneric;
}
}
String _getAuthErrorMessage(AppError error) {
switch (error.code) {
case 'invalid_credentials':
return l10n.errorInvalidCredentials;
case 'session_expired':
return l10n.errorSessionExpired;
case 'account_locked':
return l10n.errorAccountLocked;
case 'account_disabled':
return l10n.errorAccountDisabled;
case 'too_many_attempts':
return l10n.errorTooManyAttempts;
case 'password_too_weak':
return l10n.errorPasswordTooWeak;
case 'email_not_verified':
return l10n.errorEmailNotVerified;
default:
return l10n.errorAuthGeneric;
}
}
String _getValidationErrorMessage(AppError error) {
switch (error.code) {
case 'required_field':
return l10n.errorFieldRequired(error.field ?? '');
case 'invalid_email':
return l10n.errorInvalidEmail;
case 'invalid_phone':
return l10n.errorInvalidPhone;
case 'min_length':
return l10n.errorMinLength(error.minLength ?? 0);
case 'max_length':
return l10n.errorMaxLength(error.maxLength ?? 0);
case 'pattern_mismatch':
return l10n.errorInvalidFormat;
default:
return l10n.errorValidationGeneric;
}
}
String _getPermissionErrorMessage(AppError error) {
switch (error.code) {
case 'camera_denied':
return l10n.errorCameraPermissionDenied;
case 'location_denied':
return l10n.errorLocationPermissionDenied;
case 'storage_denied':
return l10n.errorStoragePermissionDenied;
case 'notification_denied':
return l10n.errorNotificationPermissionDenied;
default:
return l10n.errorPermissionGeneric;
}
}
String _getStorageErrorMessage(AppError error) {
switch (error.code) {
case 'disk_full':
return l10n.errorDiskFull;
case 'file_not_found':
return l10n.errorFileNotFound;
case 'read_only':
return l10n.errorReadOnly;
case 'corrupted_data':
return l10n.errorCorruptedData;
default:
return l10n.errorStorageGeneric;
}
}
String _getServerErrorMessage(AppError error) {
switch (error.code) {
case '400':
return l10n.errorBadRequest;
case '401':
return l10n.errorUnauthorized;
case '403':
return l10n.errorForbidden;
case '404':
return l10n.errorNotFound;
case '429':
return l10n.errorRateLimited;
case '500':
return l10n.errorServerInternal;
case '502':
case '503':
return l10n.errorServerUnavailable;
default:
return l10n.errorServerGeneric;
}
}
/// Get recovery action for error
ErrorAction? getRecoveryAction(AppError error) {
switch (error.type) {
case ErrorType.network:
return ErrorAction(
label: l10n.retry,
action: ActionType.retry,
);
case ErrorType.authentication:
if (error.code == 'session_expired') {
return ErrorAction(
label: l10n.signInAgain,
action: ActionType.relogin,
);
}
break;
case ErrorType.permission:
return ErrorAction(
label: l10n.openSettings,
action: ActionType.openSettings,
);
case ErrorType.storage:
if (error.code == 'disk_full') {
return ErrorAction(
label: l10n.freeUpSpace,
action: ActionType.openStorage,
);
}
break;
default:
break;
}
return null;
}
}
class AppError {
final ErrorType type;
final String code;
final String? message;
final String? field;
final int? minLength;
final int? maxLength;
AppError({
required this.type,
required this.code,
this.message,
this.field,
this.minLength,
this.maxLength,
});
}
enum ErrorType {
network,
authentication,
validation,
permission,
storage,
server,
unknown,
}
class ErrorAction {
final String label;
final ActionType action;
ErrorAction({required this.label, required this.action});
}
enum ActionType {
retry,
relogin,
openSettings,
openStorage,
contactSupport,
dismiss,
}
Error Display Widgets
Localized Error Banner
class LocalizedErrorBanner extends StatelessWidget {
final AppError error;
final VoidCallback? onRetry;
final VoidCallback? onDismiss;
const LocalizedErrorBanner({
required this.error,
this.onRetry,
this.onDismiss,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final errorHandler = LocalizedErrorHandler(l10n);
return MaterialBanner(
backgroundColor: _getBackgroundColor(),
leading: Icon(
_getIcon(),
color: _getIconColor(),
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
errorHandler.getMessage(error),
style: TextStyle(fontWeight: FontWeight.w500),
),
if (_getHelpText(l10n) != null)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
_getHelpText(l10n)!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
),
],
),
actions: _buildActions(context, l10n, errorHandler),
);
}
List<Widget> _buildActions(
BuildContext context,
AppLocalizations l10n,
LocalizedErrorHandler handler,
) {
final actions = <Widget>[];
final recovery = handler.getRecoveryAction(error);
if (recovery != null && onRetry != null) {
actions.add(
TextButton(
onPressed: onRetry,
child: Text(recovery.label),
),
);
}
if (onDismiss != null) {
actions.add(
TextButton(
onPressed: onDismiss,
child: Text(l10n.dismiss),
),
);
}
return actions;
}
String? _getHelpText(AppLocalizations l10n) {
switch (error.type) {
case ErrorType.network:
return l10n.helpCheckConnection;
case ErrorType.authentication:
if (error.code == 'too_many_attempts') {
return l10n.helpWaitAndRetry;
}
break;
default:
break;
}
return null;
}
Color _getBackgroundColor() {
switch (error.type) {
case ErrorType.network:
return Colors.orange[50]!;
case ErrorType.authentication:
return Colors.red[50]!;
case ErrorType.validation:
return Colors.yellow[50]!;
default:
return Colors.grey[100]!;
}
}
IconData _getIcon() {
switch (error.type) {
case ErrorType.network:
return Icons.wifi_off;
case ErrorType.authentication:
return Icons.lock_outline;
case ErrorType.validation:
return Icons.warning_amber;
case ErrorType.permission:
return Icons.block;
case ErrorType.storage:
return Icons.storage;
case ErrorType.server:
return Icons.cloud_off;
default:
return Icons.error_outline;
}
}
Color _getIconColor() {
switch (error.type) {
case ErrorType.network:
return Colors.orange;
case ErrorType.authentication:
return Colors.red;
case ErrorType.validation:
return Colors.amber[700]!;
default:
return Colors.grey[700]!;
}
}
}
Full-Screen Error State
class LocalizedErrorScreen extends StatelessWidget {
final AppError error;
final VoidCallback? onRetry;
final VoidCallback? onGoBack;
final VoidCallback? onContactSupport;
const LocalizedErrorScreen({
required this.error,
this.onRetry,
this.onGoBack,
this.onContactSupport,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final errorHandler = LocalizedErrorHandler(l10n);
return Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Illustration
_buildIllustration(),
SizedBox(height: 32),
// Title
Text(
_getTitle(l10n),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
SizedBox(height: 12),
// Message
Text(
errorHandler.getMessage(error),
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
SizedBox(height: 8),
// Help text
if (_getDetailedHelp(l10n) != null)
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
_getDetailedHelp(l10n)!,
style: TextStyle(
color: Colors.grey[500],
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 32),
// Primary action
if (onRetry != null)
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: Icon(Icons.refresh),
label: Text(l10n.tryAgain),
onPressed: onRetry,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
),
),
SizedBox(height: 12),
// Secondary actions
Row(
children: [
if (onGoBack != null)
Expanded(
child: OutlinedButton(
onPressed: onGoBack,
child: Text(l10n.goBack),
),
),
if (onGoBack != null && onContactSupport != null)
SizedBox(width: 12),
if (onContactSupport != null)
Expanded(
child: OutlinedButton(
onPressed: onContactSupport,
child: Text(l10n.contactSupport),
),
),
],
),
SizedBox(height: 24),
// Error code (for support)
if (error.code.isNotEmpty)
Text(
l10n.errorCode(error.code),
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
),
],
),
),
),
);
}
Widget _buildIllustration() {
final icon = _getIcon();
final color = _getColor();
return Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(icon, size: 60, color: color),
);
}
String _getTitle(AppLocalizations l10n) {
switch (error.type) {
case ErrorType.network:
return l10n.errorTitleNetwork;
case ErrorType.authentication:
return l10n.errorTitleAuth;
case ErrorType.server:
return l10n.errorTitleServer;
default:
return l10n.errorTitleGeneric;
}
}
String? _getDetailedHelp(AppLocalizations l10n) {
switch (error.type) {
case ErrorType.network:
return l10n.helpNetworkDetailed;
case ErrorType.server:
return l10n.helpServerDetailed;
default:
return null;
}
}
IconData _getIcon() {
switch (error.type) {
case ErrorType.network:
return Icons.signal_wifi_off;
case ErrorType.authentication:
return Icons.lock_outline;
case ErrorType.server:
return Icons.cloud_off;
default:
return Icons.error_outline;
}
}
Color _getColor() {
switch (error.type) {
case ErrorType.network:
return Colors.orange;
case ErrorType.authentication:
return Colors.red;
case ErrorType.server:
return Colors.blue;
default:
return Colors.grey;
}
}
}
Form Validation Errors
Localized Field Errors
class LocalizedFormField extends StatelessWidget {
final TextEditingController controller;
final String label;
final String? Function(String?) validator;
final TextInputType? keyboardType;
final bool obscureText;
const LocalizedFormField({
required this.controller,
required this.label,
required this.validator,
this.keyboardType,
this.obscureText = false,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: label,
errorMaxLines: 2,
),
keyboardType: keyboardType,
obscureText: obscureText,
validator: validator,
autovalidateMode: AutovalidateMode.onUserInteraction,
);
}
}
class LocalizedValidators {
final AppLocalizations l10n;
LocalizedValidators(this.l10n);
String? required(String? value, {String? fieldName}) {
if (value == null || value.trim().isEmpty) {
return fieldName != null
? l10n.errorFieldRequired(fieldName)
: l10n.errorRequired;
}
return null;
}
String? email(String? value) {
if (value == null || value.isEmpty) return null;
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return l10n.errorInvalidEmail;
}
return null;
}
String? phone(String? value) {
if (value == null || value.isEmpty) return null;
final phoneRegex = RegExp(r'^\+?[\d\s-]{10,}$');
if (!phoneRegex.hasMatch(value)) {
return l10n.errorInvalidPhone;
}
return null;
}
String? minLength(String? value, int min) {
if (value == null || value.isEmpty) return null;
if (value.length < min) {
return l10n.errorMinLength(min);
}
return null;
}
String? maxLength(String? value, int max) {
if (value == null || value.isEmpty) return null;
if (value.length > max) {
return l10n.errorMaxLength(max);
}
return null;
}
String? password(String? value) {
if (value == null || value.isEmpty) return null;
if (value.length < 8) {
return l10n.errorPasswordMinLength;
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return l10n.errorPasswordUppercase;
}
if (!value.contains(RegExp(r'[a-z]'))) {
return l10n.errorPasswordLowercase;
}
if (!value.contains(RegExp(r'[0-9]'))) {
return l10n.errorPasswordNumber;
}
return null;
}
String? confirmPassword(String? value, String password) {
if (value != password) {
return l10n.errorPasswordMismatch;
}
return null;
}
/// Combine multiple validators
String? Function(String?) combine(List<String? Function(String?)> validators) {
return (value) {
for (final validator in validators) {
final error = validator(value);
if (error != null) return error;
}
return null;
};
}
}
Crash/Fatal Error Screen
App Crash Handler
class LocalizedCrashScreen extends StatelessWidget {
final FlutterErrorDetails? errorDetails;
final VoidCallback onRestart;
final VoidCallback onReportBug;
const LocalizedCrashScreen({
this.errorDetails,
required this.onRestart,
required this.onReportBug,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Sad face illustration
Text('😵', style: TextStyle(fontSize: 64)),
SizedBox(height: 24),
Text(
l10n.crashTitle,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 12),
Text(
l10n.crashDescription,
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
SizedBox(height: 32),
// Restart button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: Icon(Icons.refresh),
label: Text(l10n.restartApp),
onPressed: onRestart,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
),
),
SizedBox(height: 12),
// Report bug
TextButton.icon(
icon: Icon(Icons.bug_report),
label: Text(l10n.reportBug),
onPressed: onReportBug,
),
SizedBox(height: 24),
// Apology
Text(
l10n.crashApology,
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
);
}
}
ARB File Structure
{
"@@locale": "en",
"errorUnknown": "Something went wrong. Please try again.",
"errorNoConnection": "No internet connection. Please check your network.",
"errorTimeout": "Request timed out. Please try again.",
"errorDnsLookup": "Could not reach the server. Check your connection.",
"errorSslCertificate": "Security certificate error. Connection not secure.",
"errorConnectionRefused": "Could not connect to server.",
"errorNetworkGeneric": "Network error. Please try again.",
"errorInvalidCredentials": "Invalid email or password.",
"errorSessionExpired": "Your session has expired. Please sign in again.",
"errorAccountLocked": "Your account has been locked. Contact support.",
"errorAccountDisabled": "Your account has been disabled.",
"errorTooManyAttempts": "Too many failed attempts. Please wait and try again.",
"errorPasswordTooWeak": "Password is too weak. Use a stronger password.",
"errorEmailNotVerified": "Please verify your email address first.",
"errorAuthGeneric": "Authentication failed. Please try again.",
"errorFieldRequired": "{field} is required",
"@errorFieldRequired": {
"placeholders": {"field": {"type": "String"}}
},
"errorRequired": "This field is required",
"errorInvalidEmail": "Please enter a valid email address",
"errorInvalidPhone": "Please enter a valid phone number",
"errorMinLength": "Must be at least {count} characters",
"@errorMinLength": {
"placeholders": {"count": {"type": "int"}}
},
"errorMaxLength": "Must be no more than {count} characters",
"@errorMaxLength": {
"placeholders": {"count": {"type": "int"}}
},
"errorInvalidFormat": "Invalid format",
"errorValidationGeneric": "Please check this field",
"errorPasswordMinLength": "Password must be at least 8 characters",
"errorPasswordUppercase": "Password must contain an uppercase letter",
"errorPasswordLowercase": "Password must contain a lowercase letter",
"errorPasswordNumber": "Password must contain a number",
"errorPasswordMismatch": "Passwords do not match",
"errorCameraPermissionDenied": "Camera access denied. Enable in Settings.",
"errorLocationPermissionDenied": "Location access denied. Enable in Settings.",
"errorStoragePermissionDenied": "Storage access denied. Enable in Settings.",
"errorNotificationPermissionDenied": "Notifications disabled. Enable in Settings.",
"errorPermissionGeneric": "Permission required. Enable in Settings.",
"errorDiskFull": "Storage is full. Free up space and try again.",
"errorFileNotFound": "File not found.",
"errorReadOnly": "Cannot save. Storage is read-only.",
"errorCorruptedData": "Data is corrupted. Please reinstall the app.",
"errorStorageGeneric": "Storage error. Please try again.",
"errorBadRequest": "Invalid request. Please try again.",
"errorUnauthorized": "Please sign in to continue.",
"errorForbidden": "You don't have permission to do this.",
"errorNotFound": "The requested item was not found.",
"errorRateLimited": "Too many requests. Please wait a moment.",
"errorServerInternal": "Server error. Our team has been notified.",
"errorServerUnavailable": "Service temporarily unavailable. Please try later.",
"errorServerGeneric": "Server error. Please try again.",
"errorTitleNetwork": "Connection Problem",
"errorTitleAuth": "Authentication Required",
"errorTitleServer": "Service Unavailable",
"errorTitleGeneric": "Something Went Wrong",
"helpCheckConnection": "Check your Wi-Fi or mobile data",
"helpWaitAndRetry": "Wait a few minutes before trying again",
"helpNetworkDetailed": "Make sure you're connected to the internet. Try switching between Wi-Fi and mobile data.",
"helpServerDetailed": "Our servers are experiencing issues. We're working to fix this. Please try again later.",
"retry": "Retry",
"tryAgain": "Try Again",
"dismiss": "Dismiss",
"goBack": "Go Back",
"signInAgain": "Sign In Again",
"openSettings": "Open Settings",
"freeUpSpace": "Free Up Space",
"contactSupport": "Contact Support",
"errorCode": "Error code: {code}",
"@errorCode": {
"placeholders": {"code": {"type": "String"}}
},
"crashTitle": "Oops! Something Crashed",
"crashDescription": "The app encountered an unexpected error and needs to restart.",
"crashApology": "We apologize for the inconvenience. This error has been logged.",
"restartApp": "Restart App",
"reportBug": "Report This Bug"
}
Conclusion
Error message localization requires:
- Centralized error handling with type-based messages
- User-friendly language not technical jargon
- Actionable recovery options like retry, settings
- Contextual help text explaining what went wrong
- Graceful crash handling with restart options
With proper error localization, users worldwide will understand issues and know how to resolve them.