Flutter TabBar Localization: Tab Labels and Navigation
TabBars are fundamental navigation components that organize content into distinct sections. Proper localization of tab labels ensures users worldwide can navigate tabbed interfaces intuitively. This guide covers everything you need to know about localizing TabBars in Flutter.
Understanding TabBar Localization
TabBar localization involves several key elements:
- Tab labels - Short, descriptive section names
- Tab icons - Visual indicators (may need localization)
- Tab tooltips - Accessibility descriptions
- Badge counts - Notification indicators
- RTL support - Right-to-left tab ordering
Setting Up TabBar Localization
ARB File Structure
{
"@@locale": "en",
"tabHome": "Home",
"@tabHome": {
"description": "Home tab label"
},
"tabSearch": "Search",
"@tabSearch": {
"description": "Search tab label"
},
"tabFavorites": "Favorites",
"@tabFavorites": {
"description": "Favorites tab label"
},
"tabProfile": "Profile",
"@tabProfile": {
"description": "Profile tab label"
},
"tabSettings": "Settings",
"@tabSettings": {
"description": "Settings tab label"
},
"tabNotifications": "Notifications",
"@tabNotifications": {
"description": "Notifications tab label"
},
"tabMessages": "Messages",
"@tabMessages": {
"description": "Messages tab label"
},
"tabActivity": "Activity",
"@tabActivity": {
"description": "Activity tab label"
},
"tabAll": "All",
"@tabAll": {
"description": "All items tab"
},
"tabRecent": "Recent",
"@tabRecent": {
"description": "Recent items tab"
},
"tabPopular": "Popular",
"@tabPopular": {
"description": "Popular items tab"
},
"tabTrending": "Trending",
"@tabTrending": {
"description": "Trending items tab"
},
"tabForYou": "For You",
"@tabForYou": {
"description": "Personalized content tab"
},
"tabFollowing": "Following",
"@tabFollowing": {
"description": "Following content tab"
},
"tabPending": "Pending",
"@tabPending": {
"description": "Pending items tab"
},
"tabCompleted": "Completed",
"@tabCompleted": {
"description": "Completed items tab"
},
"tabInProgress": "In Progress",
"@tabInProgress": {
"description": "In progress items tab"
},
"tabBadgeCount": "{count, plural, =0{} =1{1 new} other{{count} new}}",
"@tabBadgeCount": {
"description": "Tab badge count",
"placeholders": {
"count": {"type": "int"}
}
},
"tabTooltipHome": "Go to home",
"@tabTooltipHome": {
"description": "Home tab tooltip"
},
"tabTooltipSearch": "Search content",
"@tabTooltipSearch": {
"description": "Search tab tooltip"
}
}
Portuguese Translations
{
"@@locale": "pt",
"tabHome": "Início",
"tabSearch": "Buscar",
"tabFavorites": "Favoritos",
"tabProfile": "Perfil",
"tabSettings": "Configurações",
"tabNotifications": "Notificações",
"tabMessages": "Mensagens",
"tabActivity": "Atividade",
"tabAll": "Todos",
"tabRecent": "Recentes",
"tabPopular": "Popular",
"tabTrending": "Em alta",
"tabForYou": "Para você",
"tabFollowing": "Seguindo",
"tabPending": "Pendente",
"tabCompleted": "Concluído",
"tabInProgress": "Em andamento",
"tabBadgeCount": "{count, plural, =0{} =1{1 novo} other{{count} novos}}",
"tabTooltipHome": "Ir para início",
"tabTooltipSearch": "Buscar conteúdo"
}
Arabic Translations (RTL)
{
"@@locale": "ar",
"tabHome": "الرئيسية",
"tabSearch": "بحث",
"tabFavorites": "المفضلة",
"tabProfile": "الملف الشخصي",
"tabSettings": "الإعدادات",
"tabNotifications": "الإشعارات",
"tabMessages": "الرسائل",
"tabActivity": "النشاط",
"tabAll": "الكل",
"tabRecent": "الأخيرة",
"tabPopular": "الأكثر شعبية",
"tabTrending": "الرائج",
"tabForYou": "لك",
"tabFollowing": "المتابَعون",
"tabPending": "قيد الانتظار",
"tabCompleted": "مكتمل",
"tabInProgress": "قيد التنفيذ",
"tabBadgeCount": "{count, plural, =0{} =1{جديد واحد} two{جديدان} few{{count} جدد} many{{count} جديداً} other{{count} جديد}}",
"tabTooltipHome": "الذهاب للرئيسية",
"tabTooltipSearch": "بحث في المحتوى"
}
Building Localized TabBar Components
Basic Localized TabBar
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedTabBar extends StatelessWidget {
const LocalizedTabBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const Text('App'),
bottom: TabBar(
tabs: [
Tab(text: l10n.tabHome, icon: const Icon(Icons.home)),
Tab(text: l10n.tabSearch, icon: const Icon(Icons.search)),
Tab(text: l10n.tabFavorites, icon: const Icon(Icons.favorite)),
Tab(text: l10n.tabProfile, icon: const Icon(Icons.person)),
],
),
),
body: TabBarView(
children: [
Center(child: Text(l10n.tabHome)),
Center(child: Text(l10n.tabSearch)),
Center(child: Text(l10n.tabFavorites)),
Center(child: Text(l10n.tabProfile)),
],
),
),
);
}
}
TabBar with Badges
class TabBarWithBadges extends StatelessWidget {
final int notificationCount;
final int messageCount;
const TabBarWithBadges({
super.key,
this.notificationCount = 0,
this.messageCount = 0,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(text: l10n.tabHome),
_buildBadgedTab(
label: l10n.tabNotifications,
icon: Icons.notifications,
count: notificationCount,
),
_buildBadgedTab(
label: l10n.tabMessages,
icon: Icons.message,
count: messageCount,
),
Tab(text: l10n.tabProfile),
],
),
),
body: const TabBarView(
children: [
Placeholder(),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
);
}
Widget _buildBadgedTab({
required String label,
required IconData icon,
required int count,
}) {
return Tab(
child: Badge(
isLabelVisible: count > 0,
label: Text(count > 99 ? '99+' : count.toString()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon),
const SizedBox(height: 4),
Text(label),
],
),
),
);
}
}
Scrollable TabBar for Many Tabs
class ScrollableLocalizedTabBar extends StatelessWidget {
const ScrollableLocalizedTabBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final tabs = [
l10n.tabAll,
l10n.tabForYou,
l10n.tabFollowing,
l10n.tabTrending,
l10n.tabPopular,
l10n.tabRecent,
];
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
title: const Text('Content'),
bottom: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: tabs.map((label) => Tab(text: label)).toList(),
),
),
body: TabBarView(
children: tabs.map((label) {
return Center(child: Text(label));
}).toList(),
),
),
);
}
}
Bottom Navigation with TabBar
class BottomTabNavigation extends StatefulWidget {
const BottomTabNavigation({super.key});
@override
State<BottomTabNavigation> createState() => _BottomTabNavigationState();
}
class _BottomTabNavigationState extends State<BottomTabNavigation> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: const [
HomePage(),
SearchPage(),
FavoritesPage(),
ProfilePage(),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: l10n.tabHome,
tooltip: l10n.tabTooltipHome,
),
NavigationDestination(
icon: const Icon(Icons.search_outlined),
selectedIcon: const Icon(Icons.search),
label: l10n.tabSearch,
tooltip: l10n.tabTooltipSearch,
),
NavigationDestination(
icon: const Icon(Icons.favorite_outline),
selectedIcon: const Icon(Icons.favorite),
label: l10n.tabFavorites,
),
NavigationDestination(
icon: const Icon(Icons.person_outline),
selectedIcon: const Icon(Icons.person),
label: l10n.tabProfile,
),
],
),
);
}
}
Task Status Tabs
class TaskStatusTabs extends StatelessWidget {
final int pendingCount;
final int inProgressCount;
final int completedCount;
const TaskStatusTabs({
super.key,
this.pendingCount = 0,
this.inProgressCount = 0,
this.completedCount = 0,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DefaultTabController(
length: 3,
child: Column(
children: [
TabBar(
tabs: [
_buildCountTab(l10n.tabPending, pendingCount),
_buildCountTab(l10n.tabInProgress, inProgressCount),
_buildCountTab(l10n.tabCompleted, completedCount),
],
),
const Expanded(
child: TabBarView(
children: [
PendingTasksList(),
InProgressTasksList(),
CompletedTasksList(),
],
),
),
],
),
);
}
Widget _buildCountTab(String label, int count) {
return Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label),
if (count > 0) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Text(
count.toString(),
style: const TextStyle(fontSize: 12),
),
),
],
],
),
);
}
}
RTL Support
RTL-Aware TabBar
class RtlAwareTabBar extends StatelessWidget {
const RtlAwareTabBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(text: l10n.tabHome),
Tab(text: l10n.tabSearch),
Tab(text: l10n.tabProfile),
],
),
),
body: TabBarView(
// TabBarView automatically handles RTL
children: [
Center(child: Text('${l10n.tabHome} Content')),
Center(child: Text('${l10n.tabSearch} Content')),
Center(child: Text('${l10n.tabProfile} Content')),
],
),
),
);
}
}
Accessibility
Accessible TabBar
class AccessibleTabBar extends StatelessWidget {
const AccessibleTabBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Semantics(
label: l10n.tabTooltipHome,
child: Tab(
text: l10n.tabHome,
icon: const Icon(Icons.home),
),
),
Semantics(
label: l10n.tabTooltipSearch,
child: Tab(
text: l10n.tabSearch,
icon: const Icon(Icons.search),
),
),
Semantics(
label: '${l10n.tabProfile} tab',
child: Tab(
text: l10n.tabProfile,
icon: const Icon(Icons.person),
),
),
],
),
),
body: const TabBarView(
children: [
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
);
}
}
Dynamic Tab Configuration
Configurable Tabs from Data
class DynamicTabBar extends StatelessWidget {
final List<TabConfig> tabs;
const DynamicTabBar({super.key, required this.tabs});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
isScrollable: tabs.length > 4,
tabs: tabs.map((tab) {
return Tab(
text: tab.getLocalizedLabel(context),
icon: tab.icon != null ? Icon(tab.icon) : null,
);
}).toList(),
),
),
body: TabBarView(
children: tabs.map((tab) => tab.content).toList(),
),
),
);
}
}
class TabConfig {
final String labelKey;
final IconData? icon;
final Widget content;
const TabConfig({
required this.labelKey,
this.icon,
required this.content,
});
String getLocalizedLabel(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
// Map labelKey to localized string
switch (labelKey) {
case 'home':
return l10n.tabHome;
case 'search':
return l10n.tabSearch;
case 'favorites':
return l10n.tabFavorites;
case 'profile':
return l10n.tabProfile;
default:
return labelKey;
}
}
}
Testing TabBar Localization
void main() {
group('TabBar Localization', () {
testWidgets('displays localized tab labels', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('pt'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedTabBar(),
),
);
expect(find.text('Início'), findsOneWidget);
expect(find.text('Buscar'), findsOneWidget);
expect(find.text('Favoritos'), findsOneWidget);
expect(find.text('Perfil'), findsOneWidget);
});
testWidgets('tabs work correctly in RTL', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('ar'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Directionality(
textDirection: TextDirection.rtl,
child: RtlAwareTabBar(),
),
),
);
expect(find.text('الرئيسية'), findsOneWidget);
expect(find.text('بحث'), findsOneWidget);
});
});
}
Best Practices
- Keep labels short - Tabs have limited space
- Use icons consistently - Either all tabs have icons or none
- Provide tooltips - For accessibility
- Test text overflow - Some languages may have longer labels
- Support RTL - Tab order reverses automatically
- Use scrollable tabs - When you have many tabs
Conclusion
Proper TabBar localization ensures users worldwide can navigate your Flutter app's tabbed interfaces intuitively. By following these patterns, your tabs will feel native in any language.