Flutter QR Code Localization: Multilingual Scanner Feedback and Results
Build QR and barcode scanner apps with localized user feedback. This guide covers localizing scan results, error messages, instructions, and accessibility features for Flutter scanner applications.
QR Scanner Localization Needs
Scanner apps require localization for:
- Scan instructions - How to position the camera
- Result types - URL, text, contact, WiFi, etc.
- Error messages - Invalid code, camera errors
- Actions - Open, copy, share, save
- Accessibility - Screen reader announcements
Localized Scanner Screen
Complete Scanner UI
class LocalizedQRScanner extends StatefulWidget {
@override
State<LocalizedQRScanner> createState() => _LocalizedQRScannerState();
}
class _LocalizedQRScannerState extends State<LocalizedQRScanner> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? _controller;
bool _isFlashOn = false;
bool _isScanning = true;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: Stack(
children: [
// Scanner view
QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Theme.of(context).primaryColor,
borderRadius: 12,
borderLength: 30,
borderWidth: 10,
cutOutSize: MediaQuery.of(context).size.width * 0.7,
),
),
// Instructions overlay
_buildInstructions(l10n),
// Top controls
_buildTopControls(l10n),
// Bottom controls
_buildBottomControls(l10n),
],
),
);
}
Widget _buildInstructions(AppLocalizations l10n) {
return Positioned(
top: MediaQuery.of(context).padding.top + 100,
left: 0,
right: 0,
child: Semantics(
liveRegion: true,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12),
child: Text(
_isScanning ? l10n.scanInstructions : l10n.processingCode,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
shadows: [
Shadow(color: Colors.black54, blurRadius: 4),
],
),
),
),
),
);
}
Widget _buildTopControls(AppLocalizations l10n) {
return Positioned(
top: MediaQuery.of(context).padding.top,
left: 0,
right: 0,
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Close button
_buildControlButton(
icon: Icons.close,
label: l10n.close,
onPressed: () => Navigator.pop(context),
),
// Flash toggle
_buildControlButton(
icon: _isFlashOn ? Icons.flash_on : Icons.flash_off,
label: _isFlashOn ? l10n.flashOn : l10n.flashOff,
onPressed: _toggleFlash,
),
],
),
),
);
}
Widget _buildBottomControls(AppLocalizations l10n) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Gallery import
_buildControlButton(
icon: Icons.photo_library,
label: l10n.importFromGallery,
onPressed: _importFromGallery,
),
// History
_buildControlButton(
icon: Icons.history,
label: l10n.scanHistory,
onPressed: _showHistory,
),
],
),
),
),
);
}
Widget _buildControlButton({
required IconData icon,
required String label,
required VoidCallback onPressed,
}) {
return Semantics(
label: label,
button: true,
child: IconButton(
icon: Icon(icon, color: Colors.white, size: 28),
onPressed: onPressed,
tooltip: label,
),
);
}
void _onQRViewCreated(QRViewController controller) {
_controller = controller;
controller.scannedDataStream.listen((scanData) {
if (_isScanning && scanData.code != null) {
setState(() => _isScanning = false);
_handleScanResult(scanData.code!);
}
});
}
void _handleScanResult(String code) async {
// Haptic feedback
HapticFeedback.mediumImpact();
// Parse and show result
final result = QRCodeParser.parse(code);
await _showResultSheet(result);
// Resume scanning
setState(() => _isScanning = true);
}
}
QR Code Result Parser
Detecting and Localizing Result Types
class QRCodeParser {
static QRCodeResult parse(String code) {
// URL
if (_isUrl(code)) {
return QRCodeResult(
type: QRCodeType.url,
rawValue: code,
data: {'url': code},
);
}
// WiFi
if (code.startsWith('WIFI:')) {
return _parseWiFi(code);
}
// vCard contact
if (code.startsWith('BEGIN:VCARD')) {
return _parseVCard(code);
}
// Email
if (code.startsWith('mailto:') || _isEmail(code)) {
return _parseEmail(code);
}
// Phone
if (code.startsWith('tel:')) {
return QRCodeResult(
type: QRCodeType.phone,
rawValue: code,
data: {'phone': code.replaceFirst('tel:', '')},
);
}
// SMS
if (code.startsWith('sms:') || code.startsWith('smsto:')) {
return _parseSMS(code);
}
// Geo location
if (code.startsWith('geo:')) {
return _parseGeo(code);
}
// Calendar event
if (code.startsWith('BEGIN:VEVENT')) {
return _parseCalendarEvent(code);
}
// Plain text
return QRCodeResult(
type: QRCodeType.text,
rawValue: code,
data: {'text': code},
);
}
static QRCodeResult _parseWiFi(String code) {
// Format: WIFI:T:WPA;S:NetworkName;P:Password;;
final data = <String, String>{};
final regex = RegExp(r'(\w+):([^;]*);');
for (final match in regex.allMatches(code)) {
final key = match.group(1)!;
final value = match.group(2)!;
switch (key) {
case 'S':
data['ssid'] = value;
break;
case 'P':
data['password'] = value;
break;
case 'T':
data['security'] = value;
break;
case 'H':
data['hidden'] = value;
break;
}
}
return QRCodeResult(
type: QRCodeType.wifi,
rawValue: code,
data: data,
);
}
static bool _isUrl(String text) {
return Uri.tryParse(text)?.hasAbsolutePath ?? false;
}
static bool _isEmail(String text) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(text);
}
}
class QRCodeResult {
final QRCodeType type;
final String rawValue;
final Map<String, String> data;
QRCodeResult({
required this.type,
required this.rawValue,
required this.data,
});
}
enum QRCodeType {
url,
text,
wifi,
contact,
email,
phone,
sms,
geo,
calendar,
unknown,
}
Localized Result Display
Result Sheet with Actions
class LocalizedResultSheet extends StatelessWidget {
final QRCodeResult result;
const LocalizedResultSheet({required this.result});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context).toString();
return Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
_buildHeader(l10n),
SizedBox(height: 16),
// Content based on type
_buildContent(l10n, locale),
SizedBox(height: 24),
// Actions
_buildActions(context, l10n),
],
),
);
}
Widget _buildHeader(AppLocalizations l10n) {
return Row(
children: [
Icon(_getTypeIcon(), size: 32, color: _getTypeColor()),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getTypeLabel(l10n),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
l10n.scannedSuccessfully,
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
],
);
}
Widget _buildContent(AppLocalizations l10n, String locale) {
switch (result.type) {
case QRCodeType.url:
return _buildUrlContent(l10n);
case QRCodeType.wifi:
return _buildWiFiContent(l10n);
case QRCodeType.contact:
return _buildContactContent(l10n);
case QRCodeType.email:
return _buildEmailContent(l10n);
case QRCodeType.phone:
return _buildPhoneContent(l10n);
case QRCodeType.geo:
return _buildGeoContent(l10n);
case QRCodeType.calendar:
return _buildCalendarContent(l10n, locale);
default:
return _buildTextContent(l10n);
}
}
Widget _buildUrlContent(AppLocalizations l10n) {
final url = result.data['url']!;
final uri = Uri.parse(url);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.website,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
SizedBox(height: 4),
Text(
uri.host,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
Text(
url,
style: TextStyle(color: Colors.blue),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildWiFiContent(AppLocalizations l10n) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(l10n.networkName, result.data['ssid'] ?? ''),
_buildInfoRow(l10n.security, result.data['security'] ?? l10n.none),
if (result.data['password']?.isNotEmpty ?? false)
_buildInfoRow(l10n.password, '••••••••'),
if (result.data['hidden'] == 'true')
_buildInfoRow(l10n.hiddenNetwork, l10n.yes),
],
);
}
Widget _buildContactContent(AppLocalizations l10n) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (result.data['name'] != null)
_buildInfoRow(l10n.name, result.data['name']!),
if (result.data['phone'] != null)
_buildInfoRow(l10n.phone, result.data['phone']!),
if (result.data['email'] != null)
_buildInfoRow(l10n.email, result.data['email']!),
if (result.data['organization'] != null)
_buildInfoRow(l10n.organization, result.data['organization']!),
],
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: TextStyle(color: Colors.grey[600]),
),
),
Expanded(
child: Text(
value,
style: TextStyle(fontWeight: FontWeight.w500),
),
),
],
),
);
}
Widget _buildActions(BuildContext context, AppLocalizations l10n) {
final actions = _getActionsForType(l10n);
return Wrap(
spacing: 8,
runSpacing: 8,
children: actions.map((action) {
return ElevatedButton.icon(
icon: Icon(action.icon),
label: Text(action.label),
onPressed: () => action.onPressed(context),
style: action.isPrimary
? null
: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200],
foregroundColor: Colors.black87,
),
);
}).toList(),
);
}
List<QRAction> _getActionsForType(AppLocalizations l10n) {
switch (result.type) {
case QRCodeType.url:
return [
QRAction(
icon: Icons.open_in_browser,
label: l10n.openUrl,
isPrimary: true,
onPressed: (ctx) => _openUrl(ctx),
),
QRAction(
icon: Icons.copy,
label: l10n.copyLink,
onPressed: (ctx) => _copyToClipboard(ctx, l10n),
),
QRAction(
icon: Icons.share,
label: l10n.share,
onPressed: (ctx) => _share(ctx),
),
];
case QRCodeType.wifi:
return [
QRAction(
icon: Icons.wifi,
label: l10n.connectToNetwork,
isPrimary: true,
onPressed: (ctx) => _connectToWiFi(ctx, l10n),
),
QRAction(
icon: Icons.copy,
label: l10n.copyPassword,
onPressed: (ctx) => _copyPassword(ctx, l10n),
),
];
case QRCodeType.contact:
return [
QRAction(
icon: Icons.person_add,
label: l10n.addContact,
isPrimary: true,
onPressed: (ctx) => _addContact(ctx),
),
QRAction(
icon: Icons.call,
label: l10n.call,
onPressed: (ctx) => _call(ctx),
),
];
case QRCodeType.phone:
return [
QRAction(
icon: Icons.call,
label: l10n.call,
isPrimary: true,
onPressed: (ctx) => _call(ctx),
),
QRAction(
icon: Icons.message,
label: l10n.sendMessage,
onPressed: (ctx) => _sendSMS(ctx),
),
QRAction(
icon: Icons.person_add,
label: l10n.addContact,
onPressed: (ctx) => _addContact(ctx),
),
];
default:
return [
QRAction(
icon: Icons.copy,
label: l10n.copy,
isPrimary: true,
onPressed: (ctx) => _copyToClipboard(ctx, l10n),
),
QRAction(
icon: Icons.share,
label: l10n.share,
onPressed: (ctx) => _share(ctx),
),
];
}
}
String _getTypeLabel(AppLocalizations l10n) {
switch (result.type) {
case QRCodeType.url:
return l10n.typeUrl;
case QRCodeType.wifi:
return l10n.typeWifi;
case QRCodeType.contact:
return l10n.typeContact;
case QRCodeType.email:
return l10n.typeEmail;
case QRCodeType.phone:
return l10n.typePhone;
case QRCodeType.sms:
return l10n.typeSms;
case QRCodeType.geo:
return l10n.typeLocation;
case QRCodeType.calendar:
return l10n.typeEvent;
default:
return l10n.typeText;
}
}
IconData _getTypeIcon() {
switch (result.type) {
case QRCodeType.url:
return Icons.link;
case QRCodeType.wifi:
return Icons.wifi;
case QRCodeType.contact:
return Icons.person;
case QRCodeType.email:
return Icons.email;
case QRCodeType.phone:
return Icons.phone;
case QRCodeType.sms:
return Icons.sms;
case QRCodeType.geo:
return Icons.location_on;
case QRCodeType.calendar:
return Icons.event;
default:
return Icons.text_fields;
}
}
Color _getTypeColor() {
switch (result.type) {
case QRCodeType.url:
return Colors.blue;
case QRCodeType.wifi:
return Colors.green;
case QRCodeType.contact:
return Colors.purple;
case QRCodeType.email:
return Colors.orange;
case QRCodeType.phone:
return Colors.teal;
default:
return Colors.grey;
}
}
}
class QRAction {
final IconData icon;
final String label;
final bool isPrimary;
final Function(BuildContext) onPressed;
QRAction({
required this.icon,
required this.label,
this.isPrimary = false,
required this.onPressed,
});
}
Error Handling
Localized Error Messages
class ScannerErrorHandler {
static String getErrorMessage(
ScannerError error,
AppLocalizations l10n,
) {
switch (error) {
case ScannerError.cameraPermissionDenied:
return l10n.errorCameraPermission;
case ScannerError.cameraUnavailable:
return l10n.errorCameraUnavailable;
case ScannerError.invalidCode:
return l10n.errorInvalidCode;
case ScannerError.unsupportedFormat:
return l10n.errorUnsupportedFormat;
case ScannerError.imageLoadFailed:
return l10n.errorImageLoadFailed;
case ScannerError.noCodeFound:
return l10n.errorNoCodeFound;
default:
return l10n.errorGeneric;
}
}
static void showError(
BuildContext context,
ScannerError error,
AppLocalizations l10n,
) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(getErrorMessage(error, l10n)),
action: error == ScannerError.cameraPermissionDenied
? SnackBarAction(
label: l10n.openSettings,
onPressed: openAppSettings,
)
: null,
),
);
}
}
enum ScannerError {
cameraPermissionDenied,
cameraUnavailable,
invalidCode,
unsupportedFormat,
imageLoadFailed,
noCodeFound,
generic,
}
ARB File Structure
{
"@@locale": "en",
"scanInstructions": "Point camera at QR code",
"processingCode": "Processing...",
"scannedSuccessfully": "Scanned successfully",
"close": "Close",
"flashOn": "Flash on",
"flashOff": "Flash off",
"importFromGallery": "Import from gallery",
"scanHistory": "History",
"typeUrl": "Website",
"typeWifi": "WiFi Network",
"typeContact": "Contact",
"typeEmail": "Email",
"typePhone": "Phone Number",
"typeSms": "SMS",
"typeLocation": "Location",
"typeEvent": "Calendar Event",
"typeText": "Text",
"website": "Website",
"networkName": "Network Name",
"security": "Security",
"password": "Password",
"hiddenNetwork": "Hidden Network",
"name": "Name",
"phone": "Phone",
"email": "Email",
"organization": "Organization",
"none": "None",
"yes": "Yes",
"no": "No",
"openUrl": "Open",
"copyLink": "Copy Link",
"share": "Share",
"copy": "Copy",
"connectToNetwork": "Connect",
"copyPassword": "Copy Password",
"addContact": "Add Contact",
"call": "Call",
"sendMessage": "Message",
"copiedToClipboard": "Copied to clipboard",
"connectingToWifi": "Connecting to {network}...",
"@connectingToWifi": {
"placeholders": {"network": {"type": "String"}}
},
"connectedToWifi": "Connected to {network}",
"@connectedToWifi": {
"placeholders": {"network": {"type": "String"}}
},
"errorCameraPermission": "Camera permission is required to scan codes",
"errorCameraUnavailable": "Camera is not available",
"errorInvalidCode": "Invalid QR code",
"errorUnsupportedFormat": "This code format is not supported",
"errorImageLoadFailed": "Failed to load image",
"errorNoCodeFound": "No QR code found in image",
"errorGeneric": "An error occurred. Please try again.",
"openSettings": "Open Settings"
}
Conclusion
QR scanner localization requires:
- Scan instructions for user guidance
- Result type detection and labeling
- Context-appropriate actions per type
- Error messages for all failure cases
- Accessibility labels for screen readers
With comprehensive localization, your scanner app will help users worldwide decode any QR code.