← Back to Blog

Flutter Biometric Localization: Multilingual Face ID, Touch ID, and Fingerprint Prompts

flutterbiometricface-idtouch-idfingerprintlocalizationsecurity

Flutter Biometric Localization: Multilingual Face ID, Touch ID, and Fingerprint Prompts

Biometric authentication is a critical security feature in modern apps. Localizing biometric prompts correctly ensures users understand what they're authenticating and builds trust. This guide covers how to implement localized biometric authentication in Flutter for iOS and Android.

Why Biometric Localization Matters

Biometric prompts are security-critical UI elements:

  • Trust - Users must understand what they're approving
  • Legal compliance - Some regions require native language prompts
  • Accessibility - Clear instructions for all users
  • Platform guidelines - iOS and Android have specific requirements
  • Error handling - Failures must be clearly explained

Setting Up Biometric Authentication

Dependencies

# pubspec.yaml
dependencies:
  local_auth: ^2.1.0
  flutter_secure_storage: ^9.0.0

Basic Localized Biometric Service

import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart';

class LocalizedBiometricService {
  final LocalAuthentication _auth = LocalAuthentication();
  final Map<String, BiometricStrings> _localizedStrings;

  LocalizedBiometricService(this._localizedStrings);

  Future<bool> isAvailable() async {
    try {
      final canCheckBiometrics = await _auth.canCheckBiometrics;
      final isDeviceSupported = await _auth.isDeviceSupported();
      return canCheckBiometrics && isDeviceSupported;
    } catch (e) {
      return false;
    }
  }

  Future<List<BiometricType>> getAvailableBiometrics() async {
    try {
      return await _auth.getAvailableBiometrics();
    } catch (e) {
      return [];
    }
  }

  Future<BiometricResult> authenticate({
    required String locale,
    required AuthenticationPurpose purpose,
  }) async {
    final strings = _localizedStrings[locale] ?? _localizedStrings['en']!;
    final reason = _getLocalizedReason(strings, purpose);

    try {
      final authenticated = await _auth.authenticate(
        localizedReason: reason,
        options: AuthenticationOptions(
          stickyAuth: true,
          biometricOnly: false,
          useErrorDialogs: true,
        ),
        authMessages: [
          AndroidAuthMessages(
            signInTitle: strings.signInTitle,
            biometricHint: strings.biometricHint,
            biometricNotRecognized: strings.biometricNotRecognized,
            biometricSuccess: strings.biometricSuccess,
            cancelButton: strings.cancelButton,
            deviceCredentialsRequiredTitle: strings.deviceCredentialsTitle,
            deviceCredentialsSetupDescription: strings.deviceCredentialsDescription,
            goToSettingsButton: strings.goToSettings,
            goToSettingsDescription: strings.goToSettingsDescription,
          ),
          IOSAuthMessages(
            lockOut: strings.lockOut,
            goToSettingsButton: strings.goToSettings,
            goToSettingsDescription: strings.goToSettingsDescription,
            cancelButton: strings.cancelButton,
            localizedFallbackTitle: strings.fallbackTitle,
          ),
        ],
      );

      return BiometricResult(
        success: authenticated,
        message: authenticated ? strings.biometricSuccess : strings.authenticationFailed,
      );
    } on PlatformException catch (e) {
      return _handleError(e, strings);
    }
  }

  String _getLocalizedReason(BiometricStrings strings, AuthenticationPurpose purpose) {
    switch (purpose) {
      case AuthenticationPurpose.login:
        return strings.loginReason;
      case AuthenticationPurpose.payment:
        return strings.paymentReason;
      case AuthenticationPurpose.sensitiveData:
        return strings.sensitiveDataReason;
      case AuthenticationPurpose.settings:
        return strings.settingsReason;
      case AuthenticationPurpose.export:
        return strings.exportReason;
    }
  }

  BiometricResult _handleError(PlatformException e, BiometricStrings strings) {
    String message;

    switch (e.code) {
      case 'NotAvailable':
        message = strings.notAvailable;
        break;
      case 'NotEnrolled':
        message = strings.notEnrolled;
        break;
      case 'LockedOut':
        message = strings.lockedOut;
        break;
      case 'PermanentlyLockedOut':
        message = strings.permanentlyLockedOut;
        break;
      case 'PasscodeNotSet':
        message = strings.passcodeNotSet;
        break;
      default:
        message = strings.unknownError;
    }

    return BiometricResult(
      success: false,
      message: message,
      errorCode: e.code,
    );
  }
}

enum AuthenticationPurpose {
  login,
  payment,
  sensitiveData,
  settings,
  export,
}

class BiometricResult {
  final bool success;
  final String message;
  final String? errorCode;

  BiometricResult({
    required this.success,
    required this.message,
    this.errorCode,
  });
}

Localized Strings Definition

class BiometricStrings {
  // Authentication reasons
  final String loginReason;
  final String paymentReason;
  final String sensitiveDataReason;
  final String settingsReason;
  final String exportReason;

  // Android-specific
  final String signInTitle;
  final String biometricHint;
  final String biometricNotRecognized;
  final String biometricSuccess;
  final String cancelButton;
  final String deviceCredentialsTitle;
  final String deviceCredentialsDescription;
  final String goToSettings;
  final String goToSettingsDescription;

  // iOS-specific
  final String lockOut;
  final String fallbackTitle;

  // Error messages
  final String notAvailable;
  final String notEnrolled;
  final String lockedOut;
  final String permanentlyLockedOut;
  final String passcodeNotSet;
  final String unknownError;
  final String authenticationFailed;

  BiometricStrings({
    required this.loginReason,
    required this.paymentReason,
    required this.sensitiveDataReason,
    required this.settingsReason,
    required this.exportReason,
    required this.signInTitle,
    required this.biometricHint,
    required this.biometricNotRecognized,
    required this.biometricSuccess,
    required this.cancelButton,
    required this.deviceCredentialsTitle,
    required this.deviceCredentialsDescription,
    required this.goToSettings,
    required this.goToSettingsDescription,
    required this.lockOut,
    required this.fallbackTitle,
    required this.notAvailable,
    required this.notEnrolled,
    required this.lockedOut,
    required this.permanentlyLockedOut,
    required this.passcodeNotSet,
    required this.unknownError,
    required this.authenticationFailed,
  });
}

// English strings
final englishBiometricStrings = BiometricStrings(
  loginReason: 'Authenticate to access your account',
  paymentReason: 'Authenticate to confirm payment',
  sensitiveDataReason: 'Authenticate to view sensitive information',
  settingsReason: 'Authenticate to change security settings',
  exportReason: 'Authenticate to export your data',
  signInTitle: 'Authentication Required',
  biometricHint: 'Touch the fingerprint sensor',
  biometricNotRecognized: 'Fingerprint not recognized. Try again.',
  biometricSuccess: 'Authentication successful',
  cancelButton: 'Cancel',
  deviceCredentialsTitle: 'Use Device Credentials',
  deviceCredentialsDescription: 'Use your PIN, pattern, or password to authenticate',
  goToSettings: 'Go to Settings',
  goToSettingsDescription: 'Set up biometric authentication in Settings',
  lockOut: 'Too many attempts. Try again later.',
  fallbackTitle: 'Use Passcode',
  notAvailable: 'Biometric authentication is not available on this device',
  notEnrolled: 'No biometrics enrolled. Please set up fingerprint or face recognition.',
  lockedOut: 'Biometric authentication is temporarily locked. Try again later.',
  permanentlyLockedOut: 'Biometric authentication is disabled. Use your device passcode.',
  passcodeNotSet: 'No passcode set. Please set up a device passcode first.',
  unknownError: 'An error occurred. Please try again.',
  authenticationFailed: 'Authentication failed',
);

// Spanish strings
final spanishBiometricStrings = BiometricStrings(
  loginReason: 'Autentícate para acceder a tu cuenta',
  paymentReason: 'Autentícate para confirmar el pago',
  sensitiveDataReason: 'Autentícate para ver información sensible',
  settingsReason: 'Autentícate para cambiar la configuración de seguridad',
  exportReason: 'Autentícate para exportar tus datos',
  signInTitle: 'Autenticación Requerida',
  biometricHint: 'Toca el sensor de huella dactilar',
  biometricNotRecognized: 'Huella no reconocida. Inténtalo de nuevo.',
  biometricSuccess: 'Autenticación exitosa',
  cancelButton: 'Cancelar',
  deviceCredentialsTitle: 'Usar Credenciales del Dispositivo',
  deviceCredentialsDescription: 'Usa tu PIN, patrón o contraseña para autenticarte',
  goToSettings: 'Ir a Configuración',
  goToSettingsDescription: 'Configura la autenticación biométrica en Configuración',
  lockOut: 'Demasiados intentos. Inténtalo más tarde.',
  fallbackTitle: 'Usar Código',
  notAvailable: 'La autenticación biométrica no está disponible en este dispositivo',
  notEnrolled: 'No hay biometría registrada. Por favor configura huella o reconocimiento facial.',
  lockedOut: 'La autenticación biométrica está temporalmente bloqueada.',
  permanentlyLockedOut: 'La autenticación biométrica está desactivada. Usa el código del dispositivo.',
  passcodeNotSet: 'No hay código configurado. Por favor configura un código primero.',
  unknownError: 'Ocurrió un error. Por favor inténtalo de nuevo.',
  authenticationFailed: 'Autenticación fallida',
);

// German strings
final germanBiometricStrings = BiometricStrings(
  loginReason: 'Authentifizieren Sie sich, um auf Ihr Konto zuzugreifen',
  paymentReason: 'Authentifizieren Sie sich, um die Zahlung zu bestätigen',
  sensitiveDataReason: 'Authentifizieren Sie sich, um sensible Daten anzuzeigen',
  settingsReason: 'Authentifizieren Sie sich, um Sicherheitseinstellungen zu ändern',
  exportReason: 'Authentifizieren Sie sich, um Ihre Daten zu exportieren',
  signInTitle: 'Authentifizierung Erforderlich',
  biometricHint: 'Berühren Sie den Fingerabdrucksensor',
  biometricNotRecognized: 'Fingerabdruck nicht erkannt. Versuchen Sie es erneut.',
  biometricSuccess: 'Authentifizierung erfolgreich',
  cancelButton: 'Abbrechen',
  deviceCredentialsTitle: 'Geräteanmeldedaten Verwenden',
  deviceCredentialsDescription: 'Verwenden Sie Ihre PIN, Muster oder Passwort',
  goToSettings: 'Zu Einstellungen',
  goToSettingsDescription: 'Richten Sie die biometrische Authentifizierung in den Einstellungen ein',
  lockOut: 'Zu viele Versuche. Versuchen Sie es später erneut.',
  fallbackTitle: 'Code Verwenden',
  notAvailable: 'Biometrische Authentifizierung ist auf diesem Gerät nicht verfügbar',
  notEnrolled: 'Keine Biometrie registriert. Bitte richten Sie Fingerabdruck oder Gesichtserkennung ein.',
  lockedOut: 'Biometrische Authentifizierung ist vorübergehend gesperrt.',
  permanentlyLockedOut: 'Biometrische Authentifizierung ist deaktiviert. Verwenden Sie Ihren Gerätecode.',
  passcodeNotSet: 'Kein Code eingerichtet. Bitte richten Sie zuerst einen Gerätecode ein.',
  unknownError: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.',
  authenticationFailed: 'Authentifizierung fehlgeschlagen',
);

// French strings
final frenchBiometricStrings = BiometricStrings(
  loginReason: 'Authentifiez-vous pour accéder à votre compte',
  paymentReason: 'Authentifiez-vous pour confirmer le paiement',
  sensitiveDataReason: 'Authentifiez-vous pour voir les informations sensibles',
  settingsReason: 'Authentifiez-vous pour modifier les paramètres de sécurité',
  exportReason: 'Authentifiez-vous pour exporter vos données',
  signInTitle: 'Authentification Requise',
  biometricHint: 'Touchez le capteur d\'empreinte digitale',
  biometricNotRecognized: 'Empreinte non reconnue. Réessayez.',
  biometricSuccess: 'Authentification réussie',
  cancelButton: 'Annuler',
  deviceCredentialsTitle: 'Utiliser les Identifiants de l\'Appareil',
  deviceCredentialsDescription: 'Utilisez votre code PIN, schéma ou mot de passe',
  goToSettings: 'Aller aux Paramètres',
  goToSettingsDescription: 'Configurez l\'authentification biométrique dans les Paramètres',
  lockOut: 'Trop de tentatives. Réessayez plus tard.',
  fallbackTitle: 'Utiliser le Code',
  notAvailable: 'L\'authentification biométrique n\'est pas disponible sur cet appareil',
  notEnrolled: 'Aucune biométrie enregistrée. Veuillez configurer l\'empreinte ou la reconnaissance faciale.',
  lockedOut: 'L\'authentification biométrique est temporairement verrouillée.',
  permanentlyLockedOut: 'L\'authentification biométrique est désactivée. Utilisez le code de l\'appareil.',
  passcodeNotSet: 'Aucun code défini. Veuillez d\'abord définir un code.',
  unknownError: 'Une erreur s\'est produite. Veuillez réessayer.',
  authenticationFailed: 'Échec de l\'authentification',
);

// Arabic strings
final arabicBiometricStrings = BiometricStrings(
  loginReason: 'قم بالمصادقة للوصول إلى حسابك',
  paymentReason: 'قم بالمصادقة لتأكيد الدفع',
  sensitiveDataReason: 'قم بالمصادقة لعرض المعلومات الحساسة',
  settingsReason: 'قم بالمصادقة لتغيير إعدادات الأمان',
  exportReason: 'قم بالمصادقة لتصدير بياناتك',
  signInTitle: 'المصادقة مطلوبة',
  biometricHint: 'المس مستشعر بصمة الإصبع',
  biometricNotRecognized: 'لم يتم التعرف على البصمة. حاول مرة أخرى.',
  biometricSuccess: 'تمت المصادقة بنجاح',
  cancelButton: 'إلغاء',
  deviceCredentialsTitle: 'استخدام بيانات اعتماد الجهاز',
  deviceCredentialsDescription: 'استخدم رمز PIN أو النمط أو كلمة المرور',
  goToSettings: 'الذهاب إلى الإعدادات',
  goToSettingsDescription: 'قم بإعداد المصادقة البيومترية في الإعدادات',
  lockOut: 'محاولات كثيرة جداً. حاول لاحقاً.',
  fallbackTitle: 'استخدام الرمز',
  notAvailable: 'المصادقة البيومترية غير متوفرة على هذا الجهاز',
  notEnrolled: 'لم يتم تسجيل بيانات بيومترية. يرجى إعداد البصمة أو التعرف على الوجه.',
  lockedOut: 'المصادقة البيومترية مقفلة مؤقتاً.',
  permanentlyLockedOut: 'المصادقة البيومترية معطلة. استخدم رمز الجهاز.',
  passcodeNotSet: 'لم يتم تعيين رمز مرور. يرجى إعداد رمز مرور أولاً.',
  unknownError: 'حدث خطأ. يرجى المحاولة مرة أخرى.',
  authenticationFailed: 'فشلت المصادقة',
);

Biometric UI Components

Localized Biometric Button

class LocalizedBiometricButton extends StatefulWidget {
  final AuthenticationPurpose purpose;
  final VoidCallback onSuccess;
  final Function(String) onError;

  const LocalizedBiometricButton({
    Key? key,
    required this.purpose,
    required this.onSuccess,
    required this.onError,
  }) : super(key: key);

  @override
  State<LocalizedBiometricButton> createState() => _LocalizedBiometricButtonState();
}

class _LocalizedBiometricButtonState extends State<LocalizedBiometricButton> {
  bool _isAuthenticating = false;
  List<BiometricType> _availableBiometrics = [];

  @override
  void initState() {
    super.initState();
    _loadBiometrics();
  }

  Future<void> _loadBiometrics() async {
    final service = context.read<LocalizedBiometricService>();
    final biometrics = await service.getAvailableBiometrics();
    setState(() => _availableBiometrics = biometrics);
  }

  Future<void> _authenticate() async {
    if (_isAuthenticating) return;

    setState(() => _isAuthenticating = true);

    try {
      final service = context.read<LocalizedBiometricService>();
      final locale = Localizations.localeOf(context).languageCode;

      final result = await service.authenticate(
        locale: locale,
        purpose: widget.purpose,
      );

      if (result.success) {
        widget.onSuccess();
      } else {
        widget.onError(result.message);
      }
    } finally {
      if (mounted) {
        setState(() => _isAuthenticating = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return ElevatedButton.icon(
      onPressed: _isAuthenticating ? null : _authenticate,
      icon: _isAuthenticating
          ? SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : Icon(_getBiometricIcon()),
      label: Text(_getBiometricLabel(l10n)),
      style: ElevatedButton.styleFrom(
        padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
      ),
    );
  }

  IconData _getBiometricIcon() {
    if (_availableBiometrics.contains(BiometricType.face)) {
      return Icons.face;
    } else if (_availableBiometrics.contains(BiometricType.fingerprint)) {
      return Icons.fingerprint;
    } else if (_availableBiometrics.contains(BiometricType.iris)) {
      return Icons.remove_red_eye;
    }
    return Icons.lock;
  }

  String _getBiometricLabel(AppLocalizations l10n) {
    if (_availableBiometrics.contains(BiometricType.face)) {
      return l10n.useFaceId;
    } else if (_availableBiometrics.contains(BiometricType.fingerprint)) {
      return l10n.useFingerprint;
    }
    return l10n.useBiometrics;
  }
}

Biometric Setup Screen

class LocalizedBiometricSetupScreen extends StatefulWidget {
  @override
  State<LocalizedBiometricSetupScreen> createState() =>
      _LocalizedBiometricSetupScreenState();
}

class _LocalizedBiometricSetupScreenState
    extends State<LocalizedBiometricSetupScreen> {
  bool _biometricEnabled = false;
  bool _isLoading = true;
  List<BiometricType> _availableBiometrics = [];

  @override
  void initState() {
    super.initState();
    _loadSettings();
  }

  Future<void> _loadSettings() async {
    final service = context.read<LocalizedBiometricService>();
    final prefs = context.read<UserPreferences>();

    final biometrics = await service.getAvailableBiometrics();
    final enabled = prefs.biometricEnabled;

    setState(() {
      _availableBiometrics = biometrics;
      _biometricEnabled = enabled;
      _isLoading = false;
    });
  }

  Future<void> _toggleBiometric(bool enable) async {
    if (enable) {
      // Verify user can authenticate before enabling
      final service = context.read<LocalizedBiometricService>();
      final locale = Localizations.localeOf(context).languageCode;

      final result = await service.authenticate(
        locale: locale,
        purpose: AuthenticationPurpose.settings,
      );

      if (!result.success) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(result.message)),
        );
        return;
      }
    }

    final prefs = context.read<UserPreferences>();
    await prefs.setBiometricEnabled(enable);

    setState(() => _biometricEnabled = enable);
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    if (_isLoading) {
      return Scaffold(
        appBar: AppBar(title: Text(l10n.biometricSettings)),
        body: Center(child: CircularProgressIndicator()),
      );
    }

    return Scaffold(
      appBar: AppBar(title: Text(l10n.biometricSettings)),
      body: ListView(
        children: [
          // Biometric availability info
          _buildAvailabilityCard(l10n),

          Divider(),

          // Enable/disable toggle
          if (_availableBiometrics.isNotEmpty)
            SwitchListTile(
              title: Text(l10n.enableBiometric),
              subtitle: Text(_getBiometricDescription(l10n)),
              value: _biometricEnabled,
              onChanged: _toggleBiometric,
              secondary: Icon(_getBiometricIcon()),
            ),

          // Setup instructions if not enrolled
          if (_availableBiometrics.isEmpty)
            _buildSetupInstructions(l10n),
        ],
      ),
    );
  }

  Widget _buildAvailabilityCard(AppLocalizations l10n) {
    final hasFingerprint = _availableBiometrics.contains(BiometricType.fingerprint);
    final hasFace = _availableBiometrics.contains(BiometricType.face);
    final hasIris = _availableBiometrics.contains(BiometricType.iris);

    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.availableBiometrics,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            SizedBox(height: 12),
            _buildBiometricRow(
              Icons.fingerprint,
              l10n.fingerprint,
              hasFingerprint,
            ),
            _buildBiometricRow(
              Icons.face,
              l10n.faceRecognition,
              hasFace,
            ),
            _buildBiometricRow(
              Icons.remove_red_eye,
              l10n.irisScanning,
              hasIris,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBiometricRow(IconData icon, String label, bool available) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          Icon(
            icon,
            color: available ? Colors.green : Colors.grey,
          ),
          SizedBox(width: 12),
          Text(label),
          Spacer(),
          Icon(
            available ? Icons.check_circle : Icons.cancel,
            color: available ? Colors.green : Colors.grey,
            size: 20,
          ),
        ],
      ),
    );
  }

  Widget _buildSetupInstructions(AppLocalizations l10n) {
    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(Icons.info_outline, color: Colors.orange),
                SizedBox(width: 8),
                Text(
                  l10n.biometricNotSetUp,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ],
            ),
            SizedBox(height: 12),
            Text(l10n.biometricSetupInstructions),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _openDeviceSettings,
              child: Text(l10n.openSettings),
            ),
          ],
        ),
      ),
    );
  }

  IconData _getBiometricIcon() {
    if (_availableBiometrics.contains(BiometricType.face)) {
      return Icons.face;
    } else if (_availableBiometrics.contains(BiometricType.fingerprint)) {
      return Icons.fingerprint;
    }
    return Icons.lock;
  }

  String _getBiometricDescription(AppLocalizations l10n) {
    if (_availableBiometrics.contains(BiometricType.face)) {
      return l10n.useFaceIdDescription;
    } else if (_availableBiometrics.contains(BiometricType.fingerprint)) {
      return l10n.useFingerprintDescription;
    }
    return l10n.useBiometricsDescription;
  }

  Future<void> _openDeviceSettings() async {
    // Open device settings (platform-specific implementation)
  }
}

Platform-Specific Considerations

iOS Face ID Configuration

<!-- ios/Runner/Info.plist -->
<key>NSFaceIDUsageDescription</key>
<string>$(FACE_ID_USAGE_DESCRIPTION)</string>
// Configure per locale in Build Settings or use InfoPlist.strings

// en.lproj/InfoPlist.strings
// "NSFaceIDUsageDescription" = "Use Face ID to securely access your account";

// es.lproj/InfoPlist.strings
// "NSFaceIDUsageDescription" = "Usa Face ID para acceder de forma segura a tu cuenta";

// de.lproj/InfoPlist.strings
// "NSFaceIDUsageDescription" = "Verwenden Sie Face ID, um sicher auf Ihr Konto zuzugreifen";

Android Biometric Configuration

<!-- android/app/src/main/res/values/strings.xml -->
<resources>
    <string name="biometric_prompt_title">Authentication Required</string>
    <string name="biometric_prompt_subtitle">Verify your identity</string>
</resources>

<!-- android/app/src/main/res/values-es/strings.xml -->
<resources>
    <string name="biometric_prompt_title">Autenticación Requerida</string>
    <string name="biometric_prompt_subtitle">Verifica tu identidad</string>
</resources>

<!-- android/app/src/main/res/values-de/strings.xml -->
<resources>
    <string name="biometric_prompt_title">Authentifizierung Erforderlich</string>
    <string name="biometric_prompt_subtitle">Bestätigen Sie Ihre Identität</string>
</resources>

Testing Biometric Localization

Unit Tests

void main() {
  group('LocalizedBiometricService', () {
    late LocalizedBiometricService service;
    late MockLocalAuthentication mockAuth;

    setUp(() {
      mockAuth = MockLocalAuthentication();
      service = LocalizedBiometricService({
        'en': englishBiometricStrings,
        'es': spanishBiometricStrings,
        'de': germanBiometricStrings,
      });
    });

    test('returns correct localized reason for login', () {
      final englishReason = service._getLocalizedReason(
        englishBiometricStrings,
        AuthenticationPurpose.login,
      );
      expect(englishReason, 'Authenticate to access your account');

      final spanishReason = service._getLocalizedReason(
        spanishBiometricStrings,
        AuthenticationPurpose.login,
      );
      expect(spanishReason, 'Autentícate para acceder a tu cuenta');
    });

    test('handles authentication errors with localized messages', () {
      final result = service._handleError(
        PlatformException(code: 'NotEnrolled'),
        spanishBiometricStrings,
      );

      expect(result.success, false);
      expect(
        result.message,
        contains('No hay biometría registrada'),
      );
    });

    test('falls back to English for unknown locale', () async {
      final result = await service.authenticate(
        locale: 'xx', // Unknown locale
        purpose: AuthenticationPurpose.login,
      );

      // Should use English strings as fallback
      expect(result.message, isNotNull);
    });
  });
}

Best Practices

Biometric Localization Checklist

  1. Clear authentication reasons - Explain why biometric is needed
  2. Localize error messages - Help users understand failures
  3. Support fallback methods - PIN/password in user's language
  4. Test on real devices - Simulators may not show actual prompts
  5. Follow platform guidelines - iOS and Android have specific requirements
  6. Consider accessibility - Provide alternatives for users who can't use biometrics

Conclusion

Biometric localization is critical for security-focused apps serving international users. By properly localizing authentication prompts, error messages, and setup instructions, you build trust and ensure users understand what they're authenticating. Always test on real devices in each target language.

Related Resources