Flutter Align Localization: Precision Positioning for Multilingual Apps
The Align widget provides precise control over child positioning within available space. In multilingual applications, Align becomes essential for creating layouts that adapt gracefully to different languages, text directions, and content sizes while maintaining visual harmony.
Understanding Align in Localization Context
Align positions its child according to an Alignment value, which can range from -1.0 (start) to 1.0 (end) on both axes. For multilingual apps, this matters because:
- RTL languages flip the meaning of left/right alignments
- Text-heavy UI elements need directional alignment
- Different language lengths require adaptive positioning
- Visual balance varies across writing systems
Why Align Matters for Multilingual Apps
Proper alignment ensures:
- Badges and indicators: Position correctly in RTL and LTR layouts
- Floating elements: Adapt to text direction changes
- Overlay content: Stay contextually positioned
- Card decorations: Maintain visual consistency across languages
Basic Align Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAlignExample extends StatelessWidget {
const LocalizedAlignExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.cardTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(l10n.cardDescription),
],
),
),
),
Align(
alignment: AlignmentDirectional.topEnd,
child: Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.newBadge,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
}
Directional Alignment Patterns
RTL-Aware Alignment Widget
class DirectionalAlign extends StatelessWidget {
final Widget child;
final AlignmentDirectional alignment;
final double? widthFactor;
final double? heightFactor;
const DirectionalAlign({
super.key,
required this.child,
required this.alignment,
this.widthFactor,
this.heightFactor,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
child: child,
);
}
}
// Usage examples
class DirectionalAlignmentExamples extends StatelessWidget {
const DirectionalAlignmentExamples({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
// Start-aligned text (left in LTR, right in RTL)
DirectionalAlign(
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.startAlignedText),
),
const SizedBox(height: 16),
// End-aligned text (right in LTR, left in RTL)
DirectionalAlign(
alignment: AlignmentDirectional.centerEnd,
child: Text(l10n.endAlignedText),
),
const SizedBox(height: 16),
// Top-start corner
DirectionalAlign(
alignment: AlignmentDirectional.topStart,
child: Icon(Icons.info),
),
],
);
}
}
Badge Positioning with Align
Localized Badge Component
class LocalizedBadgeContainer extends StatelessWidget {
final Widget child;
final String? badgeText;
final int? badgeCount;
final bool showBadge;
const LocalizedBadgeContainer({
super.key,
required this.child,
this.badgeText,
this.badgeCount,
this.showBadge = true,
});
@override
Widget build(BuildContext context) {
if (!showBadge || (badgeText == null && badgeCount == null)) {
return child;
}
return Stack(
clipBehavior: Clip.none,
children: [
child,
Positioned(
top: -4,
right: Directionality.of(context) == TextDirection.ltr ? -4 : null,
left: Directionality.of(context) == TextDirection.rtl ? -4 : null,
child: _Badge(
text: badgeText,
count: badgeCount,
),
),
],
);
}
}
class _Badge extends StatelessWidget {
final String? text;
final int? count;
const _Badge({this.text, this.count});
@override
Widget build(BuildContext context) {
final displayText = text ?? (count != null ? '$count' : '');
return Container(
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
displayText,
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
// Usage
class NotificationIcon extends StatelessWidget {
final int unreadCount;
const NotificationIcon({
super.key,
required this.unreadCount,
});
@override
Widget build(BuildContext context) {
return LocalizedBadgeContainer(
badgeCount: unreadCount > 99 ? 99 : unreadCount,
showBadge: unreadCount > 0,
child: IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {},
),
);
}
}
Floating Action Positioning
Localized FAB Placement
class LocalizedFloatingActions extends StatelessWidget {
const LocalizedFloatingActions({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
// Main content
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => ListTile(
title: Text('${l10n.itemLabel} ${index + 1}'),
),
),
// Primary FAB - bottom end (respects RTL)
Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsets.all(16),
child: FloatingActionButton.extended(
heroTag: 'primary',
onPressed: () {},
icon: const Icon(Icons.add),
label: Text(l10n.addItemButton),
),
),
),
// Secondary FAB - above primary
Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsets.only(
bottom: 88,
right: 16,
left: 16,
),
child: FloatingActionButton.small(
heroTag: 'secondary',
onPressed: () {},
child: const Icon(Icons.filter_list),
),
),
),
],
);
}
}
Overlay Alignment
Localized Tooltip Positioning
class LocalizedTooltipOverlay extends StatelessWidget {
final Widget child;
final String tooltipText;
final bool showTooltip;
const LocalizedTooltipOverlay({
super.key,
required this.child,
required this.tooltipText,
this.showTooltip = false,
});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Stack(
clipBehavior: Clip.none,
children: [
child,
if (showTooltip)
Positioned(
bottom: -40,
left: isRtl ? null : 0,
right: isRtl ? 0 : null,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.inverseSurface,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
children: [
Icon(
Icons.info_outline,
size: 16,
color: Theme.of(context).colorScheme.onInverseSurface,
),
const SizedBox(width: 8),
Text(
tooltipText,
style: TextStyle(
color: Theme.of(context).colorScheme.onInverseSurface,
fontSize: 12,
),
),
],
),
),
),
],
);
}
}
Card Corner Decorations
Localized Ribbon Component
class LocalizedRibbonCard extends StatelessWidget {
final String ribbonText;
final Widget child;
final Color? ribbonColor;
const LocalizedRibbonCard({
super.key,
required this.ribbonText,
required this.child,
this.ribbonColor,
});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
final color = ribbonColor ?? Theme.of(context).colorScheme.primary;
return Stack(
children: [
child,
Align(
alignment: isRtl
? Alignment.topLeft
: Alignment.topRight,
child: ClipRect(
child: Transform.rotate(
angle: isRtl ? -0.785 : 0.785, // 45 degrees
child: Transform.translate(
offset: Offset(isRtl ? -30 : 30, -10),
child: Container(
width: 120,
padding: const EdgeInsets.symmetric(vertical: 4),
color: color,
child: Text(
ribbonText,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
);
}
}
// Usage
class ProductCard extends StatelessWidget {
const ProductCard({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedRibbonCard(
ribbonText: l10n.saleRibbon,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.productName,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(l10n.productPrice),
],
),
),
),
);
}
}
Status Indicator Alignment
Localized Status Badge
class LocalizedStatusIndicator extends StatelessWidget {
final Widget child;
final String status;
final Color statusColor;
const LocalizedStatusIndicator({
super.key,
required this.child,
required this.status,
required this.statusColor,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
Align(
alignment: AlignmentDirectional.bottomEnd,
child: Transform.translate(
offset: const Offset(4, 4),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
child: Text(
status,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
}
// Usage
class UserAvatar extends StatelessWidget {
final String imageUrl;
final bool isOnline;
const UserAvatar({
super.key,
required this.imageUrl,
required this.isOnline,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedStatusIndicator(
status: isOnline ? l10n.statusOnline : l10n.statusOffline,
statusColor: isOnline ? Colors.green : Colors.grey,
child: CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(imageUrl),
),
);
}
}
Align in Form Layouts
Aligned Form Actions
class LocalizedFormActions extends StatelessWidget {
final VoidCallback onSubmit;
final VoidCallback onCancel;
const LocalizedFormActions({
super.key,
required this.onSubmit,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Align(
alignment: AlignmentDirectional.centerEnd,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: onCancel,
child: Text(l10n.cancelButton),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: onSubmit,
child: Text(l10n.submitButton),
),
],
),
);
}
}
Progress Indicator Alignment
Localized Progress Overlay
class LocalizedProgressOverlay extends StatelessWidget {
final Widget child;
final bool isLoading;
final String? loadingText;
final AlignmentDirectional alignment;
const LocalizedProgressOverlay({
super.key,
required this.child,
required this.isLoading,
this.loadingText,
this.alignment = AlignmentDirectional.center,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
child,
if (isLoading)
Positioned.fill(
child: Container(
color: Colors.black26,
child: Align(
alignment: alignment,
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(loadingText ?? l10n.pleaseWait),
],
),
),
),
),
),
),
],
);
}
}
Alignment with Animation
Animated Alignment for Localized Content
class AnimatedLocalizedAlign extends StatefulWidget {
final bool isExpanded;
const AnimatedLocalizedAlign({
super.key,
required this.isExpanded,
});
@override
State<AnimatedLocalizedAlign> createState() => _AnimatedLocalizedAlignState();
}
class _AnimatedLocalizedAlignState extends State<AnimatedLocalizedAlign> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return AnimatedAlign(
alignment: widget.isExpanded
? Alignment.center
: (isRtl ? Alignment.centerRight : Alignment.centerLeft),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
widget.isExpanded ? l10n.expandedState : l10n.collapsedState,
style: Theme.of(context).textTheme.titleMedium,
),
),
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"cardTitle": "Featured Item",
"@cardTitle": {
"description": "Title for featured card"
},
"cardDescription": "This is a special item with exclusive features.",
"@cardDescription": {
"description": "Description for featured card"
},
"newBadge": "NEW",
"@newBadge": {
"description": "Badge text for new items"
},
"startAlignedText": "This text aligns to the start",
"endAlignedText": "This text aligns to the end",
"itemLabel": "Item",
"addItemButton": "Add Item",
"saleRibbon": "SALE",
"@saleRibbon": {
"description": "Ribbon text for sale items"
},
"productName": "Premium Product",
"productPrice": "$99.99",
"statusOnline": "Online",
"@statusOnline": {
"description": "Online status indicator"
},
"statusOffline": "Offline",
"@statusOffline": {
"description": "Offline status indicator"
},
"cancelButton": "Cancel",
"submitButton": "Submit",
"pleaseWait": "Please wait...",
"@pleaseWait": {
"description": "Generic loading message"
},
"expandedState": "Expanded View",
"collapsedState": "Collapsed",
"alignTopStart": "Top Start",
"alignTopCenter": "Top Center",
"alignTopEnd": "Top End",
"alignCenterStart": "Center Start",
"alignCenter": "Center",
"alignCenterEnd": "Center End",
"alignBottomStart": "Bottom Start",
"alignBottomCenter": "Bottom Center",
"alignBottomEnd": "Bottom End"
}
German (app_de.arb)
{
"@@locale": "de",
"cardTitle": "Hervorgehobener Artikel",
"cardDescription": "Dies ist ein besonderer Artikel mit exklusiven Funktionen.",
"newBadge": "NEU",
"startAlignedText": "Dieser Text ist am Anfang ausgerichtet",
"endAlignedText": "Dieser Text ist am Ende ausgerichtet",
"itemLabel": "Artikel",
"addItemButton": "Artikel hinzufügen",
"saleRibbon": "SALE",
"productName": "Premium-Produkt",
"productPrice": "99,99 €",
"statusOnline": "Online",
"statusOffline": "Offline",
"cancelButton": "Abbrechen",
"submitButton": "Absenden",
"pleaseWait": "Bitte warten...",
"expandedState": "Erweiterte Ansicht",
"collapsedState": "Eingeklappt",
"alignTopStart": "Oben Anfang",
"alignTopCenter": "Oben Mitte",
"alignTopEnd": "Oben Ende",
"alignCenterStart": "Mitte Anfang",
"alignCenter": "Mitte",
"alignCenterEnd": "Mitte Ende",
"alignBottomStart": "Unten Anfang",
"alignBottomCenter": "Unten Mitte",
"alignBottomEnd": "Unten Ende"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"cardTitle": "عنصر مميز",
"cardDescription": "هذا عنصر خاص بميزات حصرية.",
"newBadge": "جديد",
"startAlignedText": "هذا النص محاذاة للبداية",
"endAlignedText": "هذا النص محاذاة للنهاية",
"itemLabel": "عنصر",
"addItemButton": "إضافة عنصر",
"saleRibbon": "تخفيض",
"productName": "منتج متميز",
"productPrice": "٩٩٫٩٩ دولار",
"statusOnline": "متصل",
"statusOffline": "غير متصل",
"cancelButton": "إلغاء",
"submitButton": "إرسال",
"pleaseWait": "يرجى الانتظار...",
"expandedState": "عرض موسع",
"collapsedState": "مطوي",
"alignTopStart": "أعلى البداية",
"alignTopCenter": "أعلى الوسط",
"alignTopEnd": "أعلى النهاية",
"alignCenterStart": "وسط البداية",
"alignCenter": "الوسط",
"alignCenterEnd": "وسط النهاية",
"alignBottomStart": "أسفل البداية",
"alignBottomCenter": "أسفل الوسط",
"alignBottomEnd": "أسفل النهاية"
}
Best Practices Summary
Do's
- Use AlignmentDirectional instead of Alignment for RTL support
- Test badge positions in both LTR and RTL layouts
- Combine with Positioned in Stack for complex overlays
- Use widthFactor/heightFactor to size Align relative to child
- Consider visual weight when positioning elements across languages
Don'ts
- Don't use Alignment.centerLeft/Right in RTL-supporting apps
- Don't hardcode positions that should flip in RTL
- Don't ignore text overflow when aligning variable-length content
- Don't assume badge positions work the same in all languages
Accessibility Considerations
class AccessibleAlignedContent extends StatelessWidget {
const AccessibleAlignedContent({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Semantics(
container: true,
child: Stack(
children: [
Card(
child: Semantics(
label: l10n.productCardLabel,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.productName),
),
),
),
Align(
alignment: AlignmentDirectional.topEnd,
child: Semantics(
label: l10n.saleBadgeLabel,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.red,
child: Text(l10n.saleRibbon),
),
),
),
],
),
);
}
}
Conclusion
The Align widget is a powerful tool for precise positioning in Flutter layouts. By using AlignmentDirectional and understanding how different languages affect visual layout, you can create polished, adaptive interfaces that work seamlessly across all locales. Always test your aligned elements with actual translations and in RTL mode to ensure they position correctly in all scenarios.