← Back to Blog

Flutter Social Login Localization: OAuth Buttons and Provider Messages

fluttersocial-loginoauthauthenticationlocalizationfirebase

Flutter Social Login Localization: OAuth Buttons and Authentication Flows

Build authentication flows that work in any language. This guide covers localizing social login buttons, error messages, profile linking, and account management in Flutter.

Social Login Localization Needs

Authentication UIs require localization for:

  • Login buttons - "Sign in with Google/Apple/Facebook"
  • Error messages - Authentication failures
  • Account linking - Connecting multiple providers
  • Profile management - Account settings
  • Legal text - Privacy, terms acceptance

Localized Social Login Buttons

Standard Social Button Component

class LocalizedSocialButton extends StatelessWidget {
  final SocialProvider provider;
  final VoidCallback onPressed;
  final bool isLoading;

  const LocalizedSocialButton({
    required this.provider,
    required this.onPressed,
    this.isLoading = false,
  });

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

    return Semantics(
      label: _getAccessibilityLabel(l10n),
      button: true,
      child: ElevatedButton(
        onPressed: isLoading ? null : onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: _getBackgroundColor(),
          foregroundColor: _getForegroundColor(),
          padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
            side: _needsBorder() ? BorderSide(color: Colors.grey[300]!) : BorderSide.none,
          ),
        ),
        child: isLoading
            ? SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(strokeWidth: 2),
              )
            : Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  _buildIcon(),
                  SizedBox(width: 12),
                  Text(_getButtonText(l10n)),
                ],
              ),
      ),
    );
  }

  String _getButtonText(AppLocalizations l10n) {
    switch (provider) {
      case SocialProvider.google:
        return l10n.signInWithGoogle;
      case SocialProvider.apple:
        return l10n.signInWithApple;
      case SocialProvider.facebook:
        return l10n.signInWithFacebook;
      case SocialProvider.twitter:
        return l10n.signInWithTwitter;
      case SocialProvider.github:
        return l10n.signInWithGithub;
      case SocialProvider.microsoft:
        return l10n.signInWithMicrosoft;
      case SocialProvider.linkedin:
        return l10n.signInWithLinkedin;
    }
  }

  String _getAccessibilityLabel(AppLocalizations l10n) {
    switch (provider) {
      case SocialProvider.google:
        return l10n.signInWithGoogleAccessibility;
      case SocialProvider.apple:
        return l10n.signInWithAppleAccessibility;
      case SocialProvider.facebook:
        return l10n.signInWithFacebookAccessibility;
      default:
        return _getButtonText(l10n);
    }
  }

  Widget _buildIcon() {
    switch (provider) {
      case SocialProvider.google:
        return SvgPicture.asset(
          'assets/icons/google.svg',
          width: 20,
          height: 20,
        );
      case SocialProvider.apple:
        return Icon(Icons.apple, size: 24);
      case SocialProvider.facebook:
        return Icon(Icons.facebook, size: 24, color: Colors.white);
      default:
        return SizedBox(width: 20);
    }
  }

  Color _getBackgroundColor() {
    switch (provider) {
      case SocialProvider.google:
        return Colors.white;
      case SocialProvider.apple:
        return Colors.black;
      case SocialProvider.facebook:
        return Color(0xFF1877F2);
      case SocialProvider.twitter:
        return Color(0xFF1DA1F2);
      case SocialProvider.github:
        return Color(0xFF24292E);
      case SocialProvider.microsoft:
        return Color(0xFF2F2F2F);
      case SocialProvider.linkedin:
        return Color(0xFF0A66C2);
    }
  }

  Color _getForegroundColor() {
    switch (provider) {
      case SocialProvider.google:
        return Colors.black87;
      default:
        return Colors.white;
    }
  }

  bool _needsBorder() => provider == SocialProvider.google;
}

enum SocialProvider {
  google,
  apple,
  facebook,
  twitter,
  github,
  microsoft,
  linkedin,
}

Localized Login Screen

Complete Authentication Screen

class LocalizedLoginScreen extends StatefulWidget {
  @override
  State<LocalizedLoginScreen> createState() => _LocalizedLoginScreenState();
}

class _LocalizedLoginScreenState extends State<LocalizedLoginScreen> {
  bool _isLoading = false;
  SocialProvider? _loadingProvider;

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

    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(24),
          child: Column(
            children: [
              Spacer(),

              // Logo and welcome
              _buildHeader(l10n),
              SizedBox(height: 48),

              // Social login buttons
              _buildSocialButtons(l10n),
              SizedBox(height: 24),

              // Divider
              _buildDivider(l10n),
              SizedBox(height: 24),

              // Email login option
              _buildEmailOption(l10n),

              Spacer(),

              // Terms and privacy
              _buildLegalText(l10n),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildHeader(AppLocalizations l10n) {
    return Column(
      children: [
        // Logo
        Image.asset('assets/logo.png', height: 80),
        SizedBox(height: 24),
        Text(
          l10n.welcomeBack,
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        SizedBox(height: 8),
        Text(
          l10n.signInToContinue,
          style: TextStyle(color: Colors.grey[600]),
        ),
      ],
    );
  }

  Widget _buildSocialButtons(AppLocalizations l10n) {
    // Show different providers based on platform
    final providers = _getAvailableProviders();

    return Column(
      children: providers.map((provider) {
        return Padding(
          padding: EdgeInsets.only(bottom: 12),
          child: SizedBox(
            width: double.infinity,
            child: LocalizedSocialButton(
              provider: provider,
              isLoading: _loadingProvider == provider,
              onPressed: () => _signInWith(provider),
            ),
          ),
        );
      }).toList(),
    );
  }

  List<SocialProvider> _getAvailableProviders() {
    final providers = <SocialProvider>[];

    // Apple Sign In only on iOS/macOS
    if (Platform.isIOS || Platform.isMacOS) {
      providers.add(SocialProvider.apple);
    }

    // Google available everywhere
    providers.add(SocialProvider.google);

    // Facebook available but may be restricted in some regions
    if (!_isRestrictedRegion()) {
      providers.add(SocialProvider.facebook);
    }

    return providers;
  }

  bool _isRestrictedRegion() {
    final locale = Localizations.localeOf(context);
    // Facebook restricted in some countries
    final restricted = ['CN', 'IR', 'KP', 'RU'];
    return restricted.contains(locale.countryCode);
  }

  Widget _buildDivider(AppLocalizations l10n) {
    return Row(
      children: [
        Expanded(child: Divider()),
        Padding(
          padding: EdgeInsets.symmetric(horizontal: 16),
          child: Text(
            l10n.or,
            style: TextStyle(color: Colors.grey[600]),
          ),
        ),
        Expanded(child: Divider()),
      ],
    );
  }

  Widget _buildEmailOption(AppLocalizations l10n) {
    return SizedBox(
      width: double.infinity,
      child: OutlinedButton.icon(
        icon: Icon(Icons.email_outlined),
        label: Text(l10n.continueWithEmail),
        onPressed: () => _navigateToEmailLogin(),
        style: OutlinedButton.styleFrom(
          padding: EdgeInsets.symmetric(vertical: 12),
        ),
      ),
    );
  }

  Widget _buildLegalText(AppLocalizations l10n) {
    return Text.rich(
      TextSpan(
        text: l10n.bySigningIn,
        style: TextStyle(color: Colors.grey[600], fontSize: 12),
        children: [
          TextSpan(
            text: l10n.termsOfService,
            style: TextStyle(
              color: Theme.of(context).primaryColor,
              decoration: TextDecoration.underline,
            ),
            recognizer: TapGestureRecognizer()..onTap = _showTerms,
          ),
          TextSpan(text: l10n.and),
          TextSpan(
            text: l10n.privacyPolicy,
            style: TextStyle(
              color: Theme.of(context).primaryColor,
              decoration: TextDecoration.underline,
            ),
            recognizer: TapGestureRecognizer()..onTap = _showPrivacy,
          ),
        ],
      ),
      textAlign: TextAlign.center,
    );
  }

  Future<void> _signInWith(SocialProvider provider) async {
    setState(() {
      _isLoading = true;
      _loadingProvider = provider;
    });

    try {
      final authService = context.read<AuthService>();
      await authService.signInWith(provider);
      _navigateToHome();
    } on AuthException catch (e) {
      _showError(e);
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
          _loadingProvider = null;
        });
      }
    }
  }

  void _showError(AuthException error) {
    final l10n = AppLocalizations.of(context)!;
    final message = AuthErrorHandler.getMessage(error, l10n);

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        action: error.canRetry
            ? SnackBarAction(
                label: l10n.retry,
                onPressed: () => _signInWith(_loadingProvider!),
              )
            : null,
      ),
    );
  }
}

Authentication Error Handling

Localized Error Messages

class AuthErrorHandler {
  static String getMessage(AuthException error, AppLocalizations l10n) {
    switch (error.code) {
      // Account errors
      case 'account-exists-with-different-credential':
        return l10n.errorAccountExistsWithDifferentCredential;
      case 'email-already-in-use':
        return l10n.errorEmailAlreadyInUse;
      case 'user-not-found':
        return l10n.errorUserNotFound;
      case 'user-disabled':
        return l10n.errorUserDisabled;
      case 'wrong-password':
        return l10n.errorWrongPassword;

      // Credential errors
      case 'invalid-credential':
        return l10n.errorInvalidCredential;
      case 'invalid-verification-code':
        return l10n.errorInvalidVerificationCode;
      case 'invalid-verification-id':
        return l10n.errorInvalidVerificationId;

      // OAuth errors
      case 'operation-not-allowed':
        return l10n.errorProviderNotEnabled;
      case 'popup-closed-by-user':
        return l10n.errorSignInCancelled;
      case 'cancelled-popup-request':
        return l10n.errorSignInCancelled;

      // Network errors
      case 'network-request-failed':
        return l10n.errorNetworkFailed;
      case 'timeout':
        return l10n.errorTimeout;
      case 'too-many-requests':
        return l10n.errorTooManyRequests;

      // Apple Sign In specific
      case 'apple-sign-in-not-available':
        return l10n.errorAppleSignInNotAvailable;

      // Google Sign In specific
      case 'sign-in-cancelled':
        return l10n.errorSignInCancelled;
      case 'google-sign-in-failed':
        return l10n.errorGoogleSignInFailed;

      // Facebook specific
      case 'facebook-login-cancelled':
        return l10n.errorSignInCancelled;
      case 'facebook-token-invalid':
        return l10n.errorFacebookTokenInvalid;

      default:
        return l10n.errorGenericAuth;
    }
  }
}

class AuthException implements Exception {
  final String code;
  final String? message;
  final bool canRetry;

  AuthException({
    required this.code,
    this.message,
    this.canRetry = true,
  });
}

Account Linking UI

Link Multiple Providers

class LocalizedAccountLinking extends StatelessWidget {
  final User user;

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.linkedAccounts)),
      body: ListView(
        padding: EdgeInsets.all(16),
        children: [
          // Info card
          Card(
            color: Colors.blue[50],
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Row(
                children: [
                  Icon(Icons.info_outline, color: Colors.blue),
                  SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      l10n.linkedAccountsDescription,
                      style: TextStyle(color: Colors.blue[900]),
                    ),
                  ),
                ],
              ),
            ),
          ),
          SizedBox(height: 24),

          // Linked providers
          Text(
            l10n.connectedAccounts,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          SizedBox(height: 12),
          ...user.linkedProviders.map((provider) {
            return _buildLinkedProvider(context, provider, l10n);
          }),

          SizedBox(height: 24),

          // Available to link
          if (_getUnlinkedProviders().isNotEmpty) ...[
            Text(
              l10n.availableToLink,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            SizedBox(height: 12),
            ..._getUnlinkedProviders().map((provider) {
              return _buildUnlinkedProvider(context, provider, l10n);
            }),
          ],
        ],
      ),
    );
  }

  Widget _buildLinkedProvider(
    BuildContext context,
    LinkedProvider provider,
    AppLocalizations l10n,
  ) {
    return Card(
      child: ListTile(
        leading: _getProviderIcon(provider.type),
        title: Text(_getProviderName(provider.type, l10n)),
        subtitle: Text(provider.email ?? provider.displayName ?? ''),
        trailing: user.linkedProviders.length > 1
            ? TextButton(
                onPressed: () => _unlinkProvider(context, provider, l10n),
                child: Text(l10n.unlink),
              )
            : Tooltip(
                message: l10n.cannotUnlinkLastProvider,
                child: Icon(Icons.link, color: Colors.grey),
              ),
      ),
    );
  }

  Widget _buildUnlinkedProvider(
    BuildContext context,
    SocialProvider provider,
    AppLocalizations l10n,
  ) {
    return Card(
      child: ListTile(
        leading: _getProviderIcon(provider),
        title: Text(_getProviderName(provider, l10n)),
        subtitle: Text(l10n.notConnected),
        trailing: TextButton(
          onPressed: () => _linkProvider(context, provider, l10n),
          child: Text(l10n.link),
        ),
      ),
    );
  }

  Future<void> _unlinkProvider(
    BuildContext context,
    LinkedProvider provider,
    AppLocalizations l10n,
  ) async {
    final confirm = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(l10n.unlinkAccountTitle),
        content: Text(l10n.unlinkAccountMessage(
          _getProviderName(provider.type, l10n),
        )),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: Text(l10n.cancel),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: Text(l10n.unlink),
          ),
        ],
      ),
    );

    if (confirm == true) {
      try {
        await context.read<AuthService>().unlinkProvider(provider.type);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(l10n.accountUnlinked)),
        );
      } on AuthException catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(AuthErrorHandler.getMessage(e, l10n))),
        );
      }
    }
  }

  String _getProviderName(SocialProvider provider, AppLocalizations l10n) {
    switch (provider) {
      case SocialProvider.google:
        return 'Google';
      case SocialProvider.apple:
        return 'Apple';
      case SocialProvider.facebook:
        return 'Facebook';
      case SocialProvider.twitter:
        return 'X (Twitter)';
      case SocialProvider.github:
        return 'GitHub';
      case SocialProvider.microsoft:
        return 'Microsoft';
      case SocialProvider.linkedin:
        return 'LinkedIn';
    }
  }
}

ARB File Structure

{
  "@@locale": "en",

  "signInWithGoogle": "Sign in with Google",
  "signInWithApple": "Sign in with Apple",
  "signInWithFacebook": "Sign in with Facebook",
  "signInWithTwitter": "Sign in with X",
  "signInWithGithub": "Sign in with GitHub",
  "signInWithMicrosoft": "Sign in with Microsoft",
  "signInWithLinkedin": "Sign in with LinkedIn",

  "signInWithGoogleAccessibility": "Sign in using your Google account",
  "signInWithAppleAccessibility": "Sign in using your Apple ID",
  "signInWithFacebookAccessibility": "Sign in using your Facebook account",

  "welcomeBack": "Welcome Back",
  "signInToContinue": "Sign in to continue",
  "or": "or",
  "continueWithEmail": "Continue with Email",

  "bySigningIn": "By signing in, you agree to our ",
  "termsOfService": "Terms of Service",
  "and": " and ",
  "privacyPolicy": "Privacy Policy",

  "errorAccountExistsWithDifferentCredential": "An account already exists with a different sign-in method. Try signing in with a different method.",
  "errorEmailAlreadyInUse": "This email is already registered. Try signing in instead.",
  "errorUserNotFound": "No account found with this email.",
  "errorUserDisabled": "This account has been disabled. Contact support for help.",
  "errorWrongPassword": "Incorrect password. Please try again.",
  "errorInvalidCredential": "The sign-in credentials are invalid. Please try again.",
  "errorInvalidVerificationCode": "Invalid verification code. Please try again.",
  "errorInvalidVerificationId": "Verification failed. Please request a new code.",
  "errorProviderNotEnabled": "This sign-in method is not available.",
  "errorSignInCancelled": "Sign in was cancelled.",
  "errorNetworkFailed": "Network error. Please check your connection.",
  "errorTimeout": "Request timed out. Please try again.",
  "errorTooManyRequests": "Too many attempts. Please try again later.",
  "errorAppleSignInNotAvailable": "Apple Sign In is not available on this device.",
  "errorGoogleSignInFailed": "Google Sign In failed. Please try again.",
  "errorFacebookTokenInvalid": "Facebook login expired. Please try again.",
  "errorGenericAuth": "Authentication failed. Please try again.",

  "retry": "Retry",

  "linkedAccounts": "Linked Accounts",
  "linkedAccountsDescription": "Link multiple accounts to sign in with any of them.",
  "connectedAccounts": "Connected",
  "availableToLink": "Available to Link",
  "notConnected": "Not connected",
  "link": "Link",
  "unlink": "Unlink",
  "unlinkAccountTitle": "Unlink Account?",
  "unlinkAccountMessage": "Are you sure you want to unlink your {provider} account?",
  "@unlinkAccountMessage": {
    "placeholders": {"provider": {"type": "String"}}
  },
  "cannotUnlinkLastProvider": "Cannot unlink your only sign-in method",
  "accountUnlinked": "Account unlinked successfully",
  "accountLinked": "Account linked successfully"
}

Conclusion

Social login localization requires:

  1. Localized button text following platform guidelines
  2. Comprehensive error messages for all auth scenarios
  3. Account linking UI with clear explanations
  4. Legal text in user's language
  5. Accessibility labels for screen readers

With proper localization, your auth flow will onboard users worldwide smoothly.

Related Resources