← Back to Blog

Flutter QR Code Localization: Scanner UI and Barcode Feedback Messages

flutterqrcodebarcodescannerlocalizationcamera

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:

  1. Scan instructions for user guidance
  2. Result type detection and labeling
  3. Context-appropriate actions per type
  4. Error messages for all failure cases
  5. Accessibility labels for screen readers

With comprehensive localization, your scanner app will help users worldwide decode any QR code.

Related Resources