← Back to Blog

Flutter TabBar Localization: Tab Labels and Navigation

fluttertabbarnavigationlocalizationtabsbadges

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:

  1. Tab labels - Short, descriptive section names
  2. Tab icons - Visual indicators (may need localization)
  3. Tab tooltips - Accessibility descriptions
  4. Badge counts - Notification indicators
  5. 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

  1. Keep labels short - Tabs have limited space
  2. Use icons consistently - Either all tabs have icons or none
  3. Provide tooltips - For accessibility
  4. Test text overflow - Some languages may have longer labels
  5. Support RTL - Tab order reverses automatically
  6. 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.

Additional Resources