Flutter Center Localization: Centering Content in Multilingual Apps
The Center widget is one of Flutter's most frequently used layout widgets, positioning its child at the center of available space. While seemingly simple, proper centering in multilingual applications requires understanding how different languages, text directions, and content sizes affect layout.
Understanding Center in Localization Context
Center places its child in the middle of its constraints. In multilingual apps, this becomes nuanced because:
- Text length varies significantly between languages
- RTL languages change the visual perception of center
- Different scripts have varying visual weights
- Centered UI elements must adapt to translation length changes
Why Center Matters for Multilingual Apps
Centering behavior impacts user experience across languages:
- Empty states: Messages should center regardless of translation length
- Loading indicators: Must remain visually centered with any language
- Dialog content: Needs to balance visually in all locales
- Call-to-action buttons: Should maintain prominence when centered
Basic Center Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedCenterExample extends StatelessWidget {
const LocalizedCenterExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle_outline,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
l10n.successTitle,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.successMessage,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
);
}
}
Empty State with Center
Localized Empty State Component
class LocalizedEmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final String? actionLabel;
final VoidCallback? onAction;
const LocalizedEmptyState({
super.key,
required this.icon,
required this.title,
required this.description,
this.actionLabel,
this.onAction,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 24),
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Text(
description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
),
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: onAction,
child: Text(actionLabel!),
),
],
],
),
),
);
}
}
// Usage
class EmptyInboxScreen extends StatelessWidget {
const EmptyInboxScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.inboxTitle)),
body: LocalizedEmptyState(
icon: Icons.inbox_outlined,
title: l10n.emptyInboxTitle,
description: l10n.emptyInboxDescription,
actionLabel: l10n.composeMessageButton,
onAction: () {},
),
);
}
}
Loading States with Center
Localized Loading Indicator
class LocalizedLoadingState extends StatelessWidget {
final String? message;
final bool showProgress;
final double? progress;
const LocalizedLoadingState({
super.key,
this.message,
this.showProgress = false,
this.progress,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showProgress && progress != null)
SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 4,
),
)
else
const SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(),
),
const SizedBox(height: 24),
Text(
message ?? l10n.loadingMessage,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
if (showProgress && progress != null) ...[
const SizedBox(height: 8),
Text(
l10n.progressPercentage((progress! * 100).round()),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
],
],
),
);
}
}
// Usage in a FutureBuilder
class DataLoadingScreen extends StatelessWidget {
const DataLoadingScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return FutureBuilder<List<dynamic>>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return LocalizedLoadingState(
message: l10n.fetchingDataMessage,
);
}
if (snapshot.hasError) {
return LocalizedEmptyState(
icon: Icons.error_outline,
title: l10n.errorTitle,
description: l10n.errorDescription,
actionLabel: l10n.retryButton,
onAction: () {},
);
}
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
);
},
);
}
Future<List<dynamic>> fetchData() async {
await Future.delayed(const Duration(seconds: 2));
return [];
}
}
Center in Dialogs
Localized Centered Dialog
class LocalizedCenteredDialog extends StatelessWidget {
final String title;
final String content;
final List<Widget> actions;
const LocalizedCenteredDialog({
super.key,
required this.title,
required this.content,
required this.actions,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
Center(
child: Text(
content,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: actions
.expand((action) => [action, const SizedBox(width: 8)])
.take(actions.length * 2 - 1)
.toList(),
),
],
),
),
);
}
static Future<void> show(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return showDialog(
context: context,
builder: (context) => LocalizedCenteredDialog(
title: l10n.confirmationTitle,
content: l10n.confirmationMessage,
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.confirmButton),
),
],
),
);
}
}
RTL-Aware Center Patterns
Directional Center Widget
class DirectionalCenter extends StatelessWidget {
final Widget child;
final bool respectTextDirection;
final double? widthFactor;
final double? heightFactor;
const DirectionalCenter({
super.key,
required this.child,
this.respectTextDirection = true,
this.widthFactor,
this.heightFactor,
});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
// For RTL, we might want to adjust visual weight
// Most cases, true center works, but some designs need adjustment
return Center(
widthFactor: widthFactor,
heightFactor: heightFactor,
child: Directionality(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
child: child,
),
);
}
}
Center with Semantic Alignment
class LocalizedCenterWithContext extends StatelessWidget {
const LocalizedCenterWithContext({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Text(
l10n.importantNotice,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 12),
Text(
l10n.noticeContent,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
Center in Error States
Localized Error Display
class LocalizedErrorState extends StatelessWidget {
final String? errorCode;
final String title;
final String description;
final VoidCallback? onRetry;
final VoidCallback? onGoBack;
const LocalizedErrorState({
super.key,
this.errorCode,
required this.title,
required this.description,
this.onRetry,
this.onGoBack,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.error_outline,
size: 48,
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 24),
if (errorCode != null) ...[
Text(
l10n.errorCode(errorCode!),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
fontFamily: 'monospace',
),
),
const SizedBox(height: 8),
],
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Text(
description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 32),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onGoBack != null)
OutlinedButton(
onPressed: onGoBack,
child: Text(l10n.goBackButton),
),
if (onGoBack != null && onRetry != null)
const SizedBox(width: 12),
if (onRetry != null)
ElevatedButton(
onPressed: onRetry,
child: Text(l10n.retryButton),
),
],
),
],
),
),
);
}
}
Center in Onboarding
Localized Welcome Screen
class LocalizedWelcomeScreen extends StatelessWidget {
final VoidCallback onGetStarted;
const LocalizedWelcomeScreen({
super.key,
required this.onGetStarted,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(24),
),
child: Icon(
Icons.rocket_launch,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 40),
Text(
l10n.welcomeTitle,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
l10n.welcomeSubtitle,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: onGetStarted,
child: Text(l10n.getStartedButton),
),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {},
child: Text(l10n.alreadyHaveAccountButton),
),
],
),
),
),
),
);
}
}
Center with Animation
Animated Localized Center
class AnimatedLocalizedCenter extends StatefulWidget {
const AnimatedLocalizedCenter({super.key});
@override
State<AnimatedLocalizedCenter> createState() => _AnimatedLocalizedCenterState();
}
class _AnimatedLocalizedCenterState extends State<AnimatedLocalizedCenter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.celebration,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
l10n.celebrationTitle,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.celebrationMessage,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"successTitle": "Success!",
"@successTitle": {
"description": "Title shown on success screens"
},
"successMessage": "Your action was completed successfully.",
"@successMessage": {
"description": "Message shown on success screens"
},
"inboxTitle": "Inbox",
"emptyInboxTitle": "Your inbox is empty",
"emptyInboxDescription": "When you receive messages, they will appear here. Start a conversation to get going!",
"composeMessageButton": "Compose Message",
"loadingMessage": "Loading...",
"@loadingMessage": {
"description": "Generic loading message"
},
"progressPercentage": "{percent}% complete",
"@progressPercentage": {
"description": "Progress percentage display",
"placeholders": {
"percent": {
"type": "int"
}
}
},
"fetchingDataMessage": "Fetching your data...",
"errorTitle": "Something went wrong",
"errorDescription": "We encountered an error while processing your request. Please try again.",
"retryButton": "Try Again",
"goBackButton": "Go Back",
"errorCode": "Error code: {code}",
"@errorCode": {
"description": "Error code display",
"placeholders": {
"code": {
"type": "String"
}
}
},
"confirmationTitle": "Confirm Action",
"confirmationMessage": "Are you sure you want to proceed? This action cannot be undone.",
"cancelButton": "Cancel",
"confirmButton": "Confirm",
"importantNotice": "Important Notice",
"noticeContent": "Please review the following information carefully before proceeding.",
"welcomeTitle": "Welcome to Our App",
"welcomeSubtitle": "The best way to manage your tasks and stay organized every day.",
"getStartedButton": "Get Started",
"alreadyHaveAccountButton": "Already have an account? Sign in",
"celebrationTitle": "Congratulations!",
"celebrationMessage": "You've completed all your tasks for today. Great job!"
}
German (app_de.arb)
{
"@@locale": "de",
"successTitle": "Erfolg!",
"successMessage": "Ihre Aktion wurde erfolgreich abgeschlossen.",
"inboxTitle": "Posteingang",
"emptyInboxTitle": "Ihr Posteingang ist leer",
"emptyInboxDescription": "Wenn Sie Nachrichten erhalten, werden diese hier angezeigt. Starten Sie eine Unterhaltung!",
"composeMessageButton": "Nachricht verfassen",
"loadingMessage": "Wird geladen...",
"progressPercentage": "{percent}% abgeschlossen",
"fetchingDataMessage": "Ihre Daten werden abgerufen...",
"errorTitle": "Etwas ist schiefgelaufen",
"errorDescription": "Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"retryButton": "Erneut versuchen",
"goBackButton": "Zurück",
"errorCode": "Fehlercode: {code}",
"confirmationTitle": "Aktion bestätigen",
"confirmationMessage": "Sind Sie sicher, dass Sie fortfahren möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"cancelButton": "Abbrechen",
"confirmButton": "Bestätigen",
"importantNotice": "Wichtiger Hinweis",
"noticeContent": "Bitte überprüfen Sie die folgenden Informationen sorgfältig, bevor Sie fortfahren.",
"welcomeTitle": "Willkommen in unserer App",
"welcomeSubtitle": "Die beste Möglichkeit, Ihre Aufgaben zu verwalten und jeden Tag organisiert zu bleiben.",
"getStartedButton": "Loslegen",
"alreadyHaveAccountButton": "Bereits ein Konto? Anmelden",
"celebrationTitle": "Herzlichen Glückwunsch!",
"celebrationMessage": "Sie haben alle Ihre Aufgaben für heute erledigt. Großartige Arbeit!"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"successTitle": "نجاح!",
"successMessage": "تم إكمال الإجراء بنجاح.",
"inboxTitle": "صندوق الوارد",
"emptyInboxTitle": "صندوق الوارد فارغ",
"emptyInboxDescription": "عندما تستلم رسائل، ستظهر هنا. ابدأ محادثة للبدء!",
"composeMessageButton": "كتابة رسالة",
"loadingMessage": "جارٍ التحميل...",
"progressPercentage": "{percent}% مكتمل",
"fetchingDataMessage": "جارٍ جلب بياناتك...",
"errorTitle": "حدث خطأ ما",
"errorDescription": "واجهنا خطأ أثناء معالجة طلبك. يرجى المحاولة مرة أخرى.",
"retryButton": "حاول مرة أخرى",
"goBackButton": "رجوع",
"errorCode": "رمز الخطأ: {code}",
"confirmationTitle": "تأكيد الإجراء",
"confirmationMessage": "هل أنت متأكد من أنك تريد المتابعة؟ لا يمكن التراجع عن هذا الإجراء.",
"cancelButton": "إلغاء",
"confirmButton": "تأكيد",
"importantNotice": "إشعار مهم",
"noticeContent": "يرجى مراجعة المعلومات التالية بعناية قبل المتابعة.",
"welcomeTitle": "مرحباً بك في تطبيقنا",
"welcomeSubtitle": "أفضل طريقة لإدارة مهامك والبقاء منظماً كل يوم.",
"getStartedButton": "ابدأ الآن",
"alreadyHaveAccountButton": "لديك حساب بالفعل؟ تسجيل الدخول",
"celebrationTitle": "تهانينا!",
"celebrationMessage": "لقد أكملت جميع مهامك لهذا اليوم. عمل رائع!"
}
Best Practices Summary
Do's
- Always use textAlign: TextAlign.center for text inside Center widgets
- Constrain text width using ConstrainedBox for long translations
- Test with verbose languages to ensure content fits properly
- Use Center with SingleChildScrollView for content that might overflow
- Combine with SafeArea for full-screen centered content
Don'ts
- Don't assume text will fit without testing multiple languages
- Don't nest multiple Center widgets unnecessarily
- Don't forget RTL testing for bidirectional layouts
- Don't hardcode padding that might not work with longer translations
Accessibility Considerations
class AccessibleCenteredContent extends StatelessWidget {
const AccessibleCenteredContent({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Semantics(
container: true,
label: l10n.successScreenLabel,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Semantics(
image: true,
label: l10n.successIconLabel,
child: const Icon(Icons.check_circle, size: 64),
),
const SizedBox(height: 16),
Text(
l10n.successTitle,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
],
),
),
);
}
}
Conclusion
The Center widget is fundamental to creating balanced, visually appealing layouts in multilingual Flutter applications. By combining Center with proper text alignment, constrained widths, and RTL awareness, you can ensure your UI remains polished and professional across all languages. Always test centered content with your longest translations to verify the layout remains beautiful and functional.