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:
- Localized button text following platform guidelines
- Comprehensive error messages for all auth scenarios
- Account linking UI with clear explanations
- Legal text in user's language
- Accessibility labels for screen readers
With proper localization, your auth flow will onboard users worldwide smoothly.