← Back to Blog

Flutter NavigationBar Localization: Tab Labels, Badges, and Accessibility

flutternavigationnavigationbarlocalizationbadgesaccessibility

Flutter NavigationBar Localization: Tab Labels, Badges, and Accessibility

Navigation bars are the backbone of app navigation in Flutter. Whether using the bottom navigation bar or navigation rail, proper localization of labels, tooltips, and accessibility features ensures all users can navigate your app effectively. This guide covers comprehensive localization strategies for Flutter navigation components.

Understanding Navigation Localization

Navigation elements require localization for:

  • Tab labels: Short, descriptive text for each destination
  • Tooltips: Longer descriptions on hover/long-press
  • Badges: Notification counts and status indicators
  • Accessibility labels: Screen reader announcements
  • Selected state text: Active/inactive indicators

Basic NavigationBar with Localized Labels

Start with a simple localized bottom navigation:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedNavigationBar extends StatefulWidget {
  const LocalizedNavigationBar({super.key});

  @override
  State<LocalizedNavigationBar> createState() => _LocalizedNavigationBarState();
}

class _LocalizedNavigationBarState extends State<LocalizedNavigationBar> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      body: _buildBody(),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
        },
        destinations: [
          NavigationDestination(
            icon: const Icon(Icons.home_outlined),
            selectedIcon: const Icon(Icons.home),
            label: l10n.navHome,
            tooltip: l10n.navHomeTooltip,
          ),
          NavigationDestination(
            icon: const Icon(Icons.search_outlined),
            selectedIcon: const Icon(Icons.search),
            label: l10n.navSearch,
            tooltip: l10n.navSearchTooltip,
          ),
          NavigationDestination(
            icon: const Icon(Icons.favorite_outline),
            selectedIcon: const Icon(Icons.favorite),
            label: l10n.navFavorites,
            tooltip: l10n.navFavoritesTooltip,
          ),
          NavigationDestination(
            icon: const Icon(Icons.person_outline),
            selectedIcon: const Icon(Icons.person),
            label: l10n.navProfile,
            tooltip: l10n.navProfileTooltip,
          ),
        ],
      ),
    );
  }

  Widget _buildBody() {
    // Return appropriate page based on _selectedIndex
    return const SizedBox();
  }
}

ARB entries:

{
  "navHome": "Home",
  "@navHome": {
    "description": "Navigation label for home tab"
  },
  "navHomeTooltip": "Go to home page",
  "@navHomeTooltip": {
    "description": "Tooltip for home navigation item"
  },
  "navSearch": "Search",
  "@navSearch": {
    "description": "Navigation label for search tab"
  },
  "navSearchTooltip": "Search for content",
  "@navSearchTooltip": {
    "description": "Tooltip for search navigation item"
  },
  "navFavorites": "Favorites",
  "@navFavorites": {
    "description": "Navigation label for favorites tab"
  },
  "navFavoritesTooltip": "View your saved items",
  "@navFavoritesTooltip": {
    "description": "Tooltip for favorites navigation item"
  },
  "navProfile": "Profile",
  "@navProfile": {
    "description": "Navigation label for profile tab"
  },
  "navProfileTooltip": "View and edit your profile",
  "@navProfileTooltip": {
    "description": "Tooltip for profile navigation item"
  }
}

NavigationBar with Badges

Add localized badge notifications to navigation items:

class NavigationWithBadges extends StatefulWidget {
  const NavigationWithBadges({super.key});

  @override
  State<NavigationWithBadges> createState() => _NavigationWithBadgesState();
}

class _NavigationWithBadgesState extends State<NavigationWithBadges> {
  int _selectedIndex = 0;
  int _notificationCount = 5;
  int _messageCount = 12;
  bool _hasUpdates = true;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      body: _buildBody(),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
        },
        destinations: [
          NavigationDestination(
            icon: const Icon(Icons.home_outlined),
            selectedIcon: const Icon(Icons.home),
            label: l10n.navHome,
          ),
          NavigationDestination(
            icon: Badge(
              label: Text(_notificationCount.toString()),
              isLabelVisible: _notificationCount > 0,
              child: const Icon(Icons.notifications_outlined),
            ),
            selectedIcon: Badge(
              label: Text(_notificationCount.toString()),
              isLabelVisible: _notificationCount > 0,
              child: const Icon(Icons.notifications),
            ),
            label: l10n.navNotifications,
            tooltip: _notificationCount > 0
                ? l10n.navNotificationsWithCount(_notificationCount)
                : l10n.navNotificationsEmpty,
          ),
          NavigationDestination(
            icon: Badge(
              label: Text(_formatBadgeCount(_messageCount)),
              isLabelVisible: _messageCount > 0,
              child: const Icon(Icons.message_outlined),
            ),
            selectedIcon: Badge(
              label: Text(_formatBadgeCount(_messageCount)),
              isLabelVisible: _messageCount > 0,
              child: const Icon(Icons.message),
            ),
            label: l10n.navMessages,
            tooltip: _messageCount > 0
                ? l10n.navMessagesWithCount(_messageCount)
                : l10n.navMessagesEmpty,
          ),
          NavigationDestination(
            icon: Badge(
              isLabelVisible: _hasUpdates,
              child: const Icon(Icons.settings_outlined),
            ),
            selectedIcon: Badge(
              isLabelVisible: _hasUpdates,
              child: const Icon(Icons.settings),
            ),
            label: l10n.navSettings,
            tooltip: _hasUpdates
                ? l10n.navSettingsHasUpdates
                : l10n.navSettings,
          ),
        ],
      ),
    );
  }

  String _formatBadgeCount(int count) {
    if (count > 99) return '99+';
    return count.toString();
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

ARB entries:

{
  "navNotifications": "Notifications",
  "@navNotifications": {
    "description": "Navigation label for notifications tab"
  },
  "navNotificationsWithCount": "{count, plural, =1{1 new notification} other{{count} new notifications}}",
  "@navNotificationsWithCount": {
    "description": "Tooltip showing notification count",
    "placeholders": {
      "count": {
        "type": "int",
        "example": "5"
      }
    }
  },
  "navNotificationsEmpty": "No new notifications",
  "@navNotificationsEmpty": {
    "description": "Tooltip when no notifications"
  },
  "navMessages": "Messages",
  "@navMessages": {
    "description": "Navigation label for messages tab"
  },
  "navMessagesWithCount": "{count, plural, =1{1 unread message} other{{count} unread messages}}",
  "@navMessagesWithCount": {
    "description": "Tooltip showing message count",
    "placeholders": {
      "count": {
        "type": "int",
        "example": "12"
      }
    }
  },
  "navMessagesEmpty": "No unread messages",
  "@navMessagesEmpty": {
    "description": "Tooltip when no messages"
  },
  "navSettings": "Settings",
  "@navSettings": {
    "description": "Navigation label for settings tab"
  },
  "navSettingsHasUpdates": "Settings - Updates available",
  "@navSettingsHasUpdates": {
    "description": "Tooltip when settings has updates"
  }
}

NavigationRail for Larger Screens

Implement a navigation rail with localized labels:

class ResponsiveNavigation extends StatefulWidget {
  const ResponsiveNavigation({super.key});

  @override
  State<ResponsiveNavigation> createState() => _ResponsiveNavigationState();
}

class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final isWideScreen = MediaQuery.of(context).size.width >= 600;

    return Scaffold(
      body: Row(
        children: [
          if (isWideScreen)
            NavigationRail(
              selectedIndex: _selectedIndex,
              onDestinationSelected: (index) {
                setState(() => _selectedIndex = index);
              },
              labelType: NavigationRailLabelType.all,
              leading: FloatingActionButton(
                onPressed: () {},
                tooltip: l10n.createNewItem,
                child: const Icon(Icons.add),
              ),
              destinations: [
                NavigationRailDestination(
                  icon: const Icon(Icons.dashboard_outlined),
                  selectedIcon: const Icon(Icons.dashboard),
                  label: Text(l10n.navDashboard),
                ),
                NavigationRailDestination(
                  icon: const Icon(Icons.folder_outlined),
                  selectedIcon: const Icon(Icons.folder),
                  label: Text(l10n.navProjects),
                ),
                NavigationRailDestination(
                  icon: const Icon(Icons.people_outline),
                  selectedIcon: const Icon(Icons.people),
                  label: Text(l10n.navTeam),
                ),
                NavigationRailDestination(
                  icon: const Icon(Icons.analytics_outlined),
                  selectedIcon: const Icon(Icons.analytics),
                  label: Text(l10n.navAnalytics),
                ),
                NavigationRailDestination(
                  icon: const Icon(Icons.settings_outlined),
                  selectedIcon: const Icon(Icons.settings),
                  label: Text(l10n.navSettings),
                ),
              ],
            ),
          Expanded(
            child: _buildBody(),
          ),
        ],
      ),
      bottomNavigationBar: isWideScreen
          ? null
          : NavigationBar(
              selectedIndex: _selectedIndex,
              onDestinationSelected: (index) {
                setState(() => _selectedIndex = index);
              },
              destinations: [
                NavigationDestination(
                  icon: const Icon(Icons.dashboard_outlined),
                  selectedIcon: const Icon(Icons.dashboard),
                  label: l10n.navDashboard,
                ),
                NavigationDestination(
                  icon: const Icon(Icons.folder_outlined),
                  selectedIcon: const Icon(Icons.folder),
                  label: l10n.navProjects,
                ),
                NavigationDestination(
                  icon: const Icon(Icons.people_outline),
                  selectedIcon: const Icon(Icons.people),
                  label: l10n.navTeam,
                ),
                NavigationDestination(
                  icon: const Icon(Icons.more_horiz),
                  label: l10n.navMore,
                ),
              ],
            ),
    );
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

ARB entries:

{
  "createNewItem": "Create new item",
  "@createNewItem": {
    "description": "Tooltip for floating action button"
  },
  "navDashboard": "Dashboard",
  "@navDashboard": {
    "description": "Navigation label for dashboard"
  },
  "navProjects": "Projects",
  "@navProjects": {
    "description": "Navigation label for projects"
  },
  "navTeam": "Team",
  "@navTeam": {
    "description": "Navigation label for team"
  },
  "navAnalytics": "Analytics",
  "@navAnalytics": {
    "description": "Navigation label for analytics"
  },
  "navMore": "More",
  "@navMore": {
    "description": "Navigation label for overflow menu"
  }
}

Accessible Navigation with Semantics

Enhance accessibility with proper semantic labels:

class AccessibleNavigation extends StatefulWidget {
  const AccessibleNavigation({super.key});

  @override
  State<AccessibleNavigation> createState() => _AccessibleNavigationState();
}

class _AccessibleNavigationState extends State<AccessibleNavigation> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      body: _buildBody(),
      bottomNavigationBar: Semantics(
        label: l10n.mainNavigation,
        child: NavigationBar(
          selectedIndex: _selectedIndex,
          onDestinationSelected: (index) {
            setState(() => _selectedIndex = index);
            _announceNavigation(context, index, l10n);
          },
          destinations: [
            _buildAccessibleDestination(
              context: context,
              icon: Icons.home_outlined,
              selectedIcon: Icons.home,
              label: l10n.navHome,
              semanticLabel: l10n.navHomeAccessibility,
              isSelected: _selectedIndex == 0,
              index: 0,
              total: 4,
              l10n: l10n,
            ),
            _buildAccessibleDestination(
              context: context,
              icon: Icons.explore_outlined,
              selectedIcon: Icons.explore,
              label: l10n.navExplore,
              semanticLabel: l10n.navExploreAccessibility,
              isSelected: _selectedIndex == 1,
              index: 1,
              total: 4,
              l10n: l10n,
            ),
            _buildAccessibleDestination(
              context: context,
              icon: Icons.library_books_outlined,
              selectedIcon: Icons.library_books,
              label: l10n.navLibrary,
              semanticLabel: l10n.navLibraryAccessibility,
              isSelected: _selectedIndex == 2,
              index: 2,
              total: 4,
              l10n: l10n,
            ),
            _buildAccessibleDestination(
              context: context,
              icon: Icons.account_circle_outlined,
              selectedIcon: Icons.account_circle,
              label: l10n.navAccount,
              semanticLabel: l10n.navAccountAccessibility,
              isSelected: _selectedIndex == 3,
              index: 3,
              total: 4,
              l10n: l10n,
            ),
          ],
        ),
      ),
    );
  }

  NavigationDestination _buildAccessibleDestination({
    required BuildContext context,
    required IconData icon,
    required IconData selectedIcon,
    required String label,
    required String semanticLabel,
    required bool isSelected,
    required int index,
    required int total,
    required AppLocalizations l10n,
  }) {
    return NavigationDestination(
      icon: Semantics(
        label: l10n.navTabPosition(index + 1, total, label),
        selected: isSelected,
        child: Icon(icon),
      ),
      selectedIcon: Semantics(
        label: l10n.navTabPositionSelected(index + 1, total, label),
        selected: true,
        child: Icon(selectedIcon),
      ),
      label: label,
      tooltip: semanticLabel,
    );
  }

  void _announceNavigation(
    BuildContext context,
    int index,
    AppLocalizations l10n,
  ) {
    final labels = [
      l10n.navHome,
      l10n.navExplore,
      l10n.navLibrary,
      l10n.navAccount,
    ];

    SemanticsService.announce(
      l10n.navigatedTo(labels[index]),
      TextDirection.ltr,
    );
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

ARB entries:

{
  "mainNavigation": "Main navigation",
  "@mainNavigation": {
    "description": "Accessibility label for navigation bar"
  },
  "navExplore": "Explore",
  "@navExplore": {
    "description": "Navigation label for explore tab"
  },
  "navExploreAccessibility": "Explore and discover new content",
  "@navExploreAccessibility": {
    "description": "Accessibility tooltip for explore tab"
  },
  "navLibrary": "Library",
  "@navLibrary": {
    "description": "Navigation label for library tab"
  },
  "navLibraryAccessibility": "Your saved content and downloads",
  "@navLibraryAccessibility": {
    "description": "Accessibility tooltip for library tab"
  },
  "navAccount": "Account",
  "@navAccount": {
    "description": "Navigation label for account tab"
  },
  "navAccountAccessibility": "Manage your account settings",
  "@navAccountAccessibility": {
    "description": "Accessibility tooltip for account tab"
  },
  "navHomeAccessibility": "Return to home screen",
  "@navHomeAccessibility": {
    "description": "Accessibility tooltip for home tab"
  },
  "navTabPosition": "Tab {position} of {total}, {label}",
  "@navTabPosition": {
    "description": "Accessibility position announcement",
    "placeholders": {
      "position": {
        "type": "int",
        "example": "1"
      },
      "total": {
        "type": "int",
        "example": "4"
      },
      "label": {
        "type": "String",
        "example": "Home"
      }
    }
  },
  "navTabPositionSelected": "Tab {position} of {total}, {label}, selected",
  "@navTabPositionSelected": {
    "description": "Accessibility announcement for selected tab",
    "placeholders": {
      "position": {
        "type": "int",
        "example": "1"
      },
      "total": {
        "type": "int",
        "example": "4"
      },
      "label": {
        "type": "String",
        "example": "Home"
      }
    }
  },
  "navigatedTo": "Navigated to {destination}",
  "@navigatedTo": {
    "description": "Announcement when navigation changes",
    "placeholders": {
      "destination": {
        "type": "String",
        "example": "Home"
      }
    }
  }
}

Dynamic Navigation Based on User Role

Customize navigation based on user permissions:

class RoleBasedNavigation extends StatefulWidget {
  final UserRole userRole;

  const RoleBasedNavigation({
    super.key,
    required this.userRole,
  });

  @override
  State<RoleBasedNavigation> createState() => _RoleBasedNavigationState();
}

enum UserRole { guest, user, admin }

class _RoleBasedNavigationState extends State<RoleBasedNavigation> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final destinations = _getDestinationsForRole(l10n);

    return Scaffold(
      body: _buildBody(),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
        },
        destinations: destinations,
      ),
    );
  }

  List<NavigationDestination> _getDestinationsForRole(AppLocalizations l10n) {
    final List<NavigationDestination> destinations = [
      NavigationDestination(
        icon: const Icon(Icons.home_outlined),
        selectedIcon: const Icon(Icons.home),
        label: l10n.navHome,
      ),
    ];

    switch (widget.userRole) {
      case UserRole.guest:
        destinations.addAll([
          NavigationDestination(
            icon: const Icon(Icons.explore_outlined),
            selectedIcon: const Icon(Icons.explore),
            label: l10n.navExplore,
          ),
          NavigationDestination(
            icon: const Icon(Icons.login),
            label: l10n.navSignIn,
          ),
        ]);
        break;

      case UserRole.user:
        destinations.addAll([
          NavigationDestination(
            icon: const Icon(Icons.explore_outlined),
            selectedIcon: const Icon(Icons.explore),
            label: l10n.navExplore,
          ),
          NavigationDestination(
            icon: const Icon(Icons.favorite_outline),
            selectedIcon: const Icon(Icons.favorite),
            label: l10n.navFavorites,
          ),
          NavigationDestination(
            icon: const Icon(Icons.person_outline),
            selectedIcon: const Icon(Icons.person),
            label: l10n.navProfile,
          ),
        ]);
        break;

      case UserRole.admin:
        destinations.addAll([
          NavigationDestination(
            icon: const Icon(Icons.dashboard_outlined),
            selectedIcon: const Icon(Icons.dashboard),
            label: l10n.navDashboard,
          ),
          NavigationDestination(
            icon: const Icon(Icons.people_outline),
            selectedIcon: const Icon(Icons.people),
            label: l10n.navUsers,
          ),
          NavigationDestination(
            icon: const Icon(Icons.admin_panel_settings_outlined),
            selectedIcon: const Icon(Icons.admin_panel_settings),
            label: l10n.navAdmin,
          ),
        ]);
        break;
    }

    return destinations;
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

ARB entries:

{
  "navSignIn": "Sign In",
  "@navSignIn": {
    "description": "Navigation label for sign in (guests)"
  },
  "navUsers": "Users",
  "@navUsers": {
    "description": "Navigation label for user management (admin)"
  },
  "navAdmin": "Admin",
  "@navAdmin": {
    "description": "Navigation label for admin panel"
  }
}

RTL Support for Navigation

Handle right-to-left layouts properly:

class RTLAwareNavigation extends StatefulWidget {
  const RTLAwareNavigation({super.key});

  @override
  State<RTLAwareNavigation> createState() => _RTLAwareNavigationState();
}

class _RTLAwareNavigationState extends State<RTLAwareNavigation> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final isRTL = Directionality.of(context) == TextDirection.rtl;

    return Scaffold(
      body: Row(
        children: [
          // Navigation rail on appropriate side for RTL
          if (MediaQuery.of(context).size.width >= 600)
            Directionality(
              textDirection: Directionality.of(context),
              child: NavigationRail(
                selectedIndex: _selectedIndex,
                onDestinationSelected: (index) {
                  setState(() => _selectedIndex = index);
                },
                labelType: NavigationRailLabelType.all,
                destinations: [
                  NavigationRailDestination(
                    icon: const Icon(Icons.home_outlined),
                    selectedIcon: const Icon(Icons.home),
                    label: Text(l10n.navHome),
                  ),
                  NavigationRailDestination(
                    icon: const Icon(Icons.search_outlined),
                    selectedIcon: const Icon(Icons.search),
                    label: Text(l10n.navSearch),
                  ),
                  NavigationRailDestination(
                    icon: Icon(isRTL
                        ? Icons.arrow_back
                        : Icons.arrow_forward),
                    label: Text(l10n.navNext),
                  ),
                ],
              ),
            ),
          Expanded(
            child: _buildBody(),
          ),
        ],
      ),
      bottomNavigationBar: MediaQuery.of(context).size.width < 600
          ? NavigationBar(
              selectedIndex: _selectedIndex,
              onDestinationSelected: (index) {
                setState(() => _selectedIndex = index);
              },
              destinations: [
                NavigationDestination(
                  icon: const Icon(Icons.home_outlined),
                  selectedIcon: const Icon(Icons.home),
                  label: l10n.navHome,
                ),
                NavigationDestination(
                  icon: const Icon(Icons.search_outlined),
                  selectedIcon: const Icon(Icons.search),
                  label: l10n.navSearch,
                ),
                NavigationDestination(
                  // Use directional icon
                  icon: Icon(isRTL
                      ? Icons.arrow_back
                      : Icons.arrow_forward),
                  label: l10n.navNext,
                ),
              ],
            )
          : null,
    );
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

ARB entries:

{
  "navNext": "Next",
  "@navNext": {
    "description": "Navigation label for next/forward action"
  }
}

Navigation with Animated Badges

Create animated badge indicators:

class AnimatedBadgeNavigation extends StatefulWidget {
  const AnimatedBadgeNavigation({super.key});

  @override
  State<AnimatedBadgeNavigation> createState() =>
      _AnimatedBadgeNavigationState();
}

class _AnimatedBadgeNavigationState extends State<AnimatedBadgeNavigation>
    with SingleTickerProviderStateMixin {
  int _selectedIndex = 0;
  int _unreadCount = 0;
  late AnimationController _badgeAnimationController;
  late Animation<double> _badgeAnimation;

  @override
  void initState() {
    super.initState();
    _badgeAnimationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _badgeAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
      CurvedAnimation(
        parent: _badgeAnimationController,
        curve: Curves.elasticOut,
      ),
    );

    // Simulate receiving notifications
    Future.delayed(const Duration(seconds: 2), () {
      if (mounted) {
        _addNotification();
      }
    });
  }

  @override
  void dispose() {
    _badgeAnimationController.dispose();
    super.dispose();
  }

  void _addNotification() {
    setState(() => _unreadCount++);
    _badgeAnimationController.forward().then((_) {
      _badgeAnimationController.reverse();
    });
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      body: _buildBody(),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() {
            _selectedIndex = index;
            if (index == 1) {
              // Clear notifications when tab is selected
              _unreadCount = 0;
            }
          });
        },
        destinations: [
          NavigationDestination(
            icon: const Icon(Icons.home_outlined),
            selectedIcon: const Icon(Icons.home),
            label: l10n.navHome,
          ),
          NavigationDestination(
            icon: AnimatedBuilder(
              animation: _badgeAnimation,
              builder: (context, child) => Transform.scale(
                scale: _unreadCount > 0 ? _badgeAnimation.value : 1.0,
                child: Badge(
                  label: Text(_unreadCount.toString()),
                  isLabelVisible: _unreadCount > 0,
                  child: const Icon(Icons.notifications_outlined),
                ),
              ),
            ),
            selectedIcon: Badge(
              label: Text(_unreadCount.toString()),
              isLabelVisible: _unreadCount > 0,
              child: const Icon(Icons.notifications),
            ),
            label: l10n.navNotifications,
            tooltip: _unreadCount > 0
                ? l10n.navNotificationsWithCount(_unreadCount)
                : l10n.navNotificationsEmpty,
          ),
          NavigationDestination(
            icon: const Icon(Icons.person_outline),
            selectedIcon: const Icon(Icons.person),
            label: l10n.navProfile,
          ),
        ],
      ),
    );
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

Drawer Navigation Alternative

For apps with many destinations, use a navigation drawer:

class DrawerNavigation extends StatefulWidget {
  const DrawerNavigation({super.key});

  @override
  State<DrawerNavigation> createState() => _DrawerNavigationState();
}

class _DrawerNavigationState extends State<DrawerNavigation> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(_getPageTitle(l10n)),
      ),
      drawer: NavigationDrawer(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
          Navigator.pop(context); // Close drawer
        },
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
            child: Text(
              l10n.navigationDrawerHeader,
              style: Theme.of(context).textTheme.titleSmall,
            ),
          ),
          NavigationDrawerDestination(
            icon: const Icon(Icons.inbox_outlined),
            selectedIcon: const Icon(Icons.inbox),
            label: Text(l10n.navInbox),
          ),
          NavigationDrawerDestination(
            icon: const Icon(Icons.send_outlined),
            selectedIcon: const Icon(Icons.send),
            label: Text(l10n.navSent),
          ),
          NavigationDrawerDestination(
            icon: const Icon(Icons.drafts_outlined),
            selectedIcon: const Icon(Icons.drafts),
            label: Text(l10n.navDrafts),
          ),
          NavigationDrawerDestination(
            icon: const Icon(Icons.delete_outline),
            selectedIcon: const Icon(Icons.delete),
            label: Text(l10n.navTrash),
          ),
          const Divider(),
          Padding(
            padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
            child: Text(
              l10n.navigationDrawerLabels,
              style: Theme.of(context).textTheme.titleSmall,
            ),
          ),
          NavigationDrawerDestination(
            icon: Icon(Icons.label_outline, color: Colors.red.shade400),
            label: Text(l10n.labelWork),
          ),
          NavigationDrawerDestination(
            icon: Icon(Icons.label_outline, color: Colors.green.shade400),
            label: Text(l10n.labelPersonal),
          ),
          NavigationDrawerDestination(
            icon: Icon(Icons.label_outline, color: Colors.blue.shade400),
            label: Text(l10n.labelImportant),
          ),
        ],
      ),
      body: _buildBody(),
    );
  }

  String _getPageTitle(AppLocalizations l10n) {
    switch (_selectedIndex) {
      case 0:
        return l10n.navInbox;
      case 1:
        return l10n.navSent;
      case 2:
        return l10n.navDrafts;
      case 3:
        return l10n.navTrash;
      default:
        return l10n.appName;
    }
  }

  Widget _buildBody() {
    return const SizedBox();
  }
}

ARB entries:

{
  "navigationDrawerHeader": "Mail",
  "@navigationDrawerHeader": {
    "description": "Header text in navigation drawer"
  },
  "navigationDrawerLabels": "Labels",
  "@navigationDrawerLabels": {
    "description": "Labels section header in drawer"
  },
  "navInbox": "Inbox",
  "@navInbox": {
    "description": "Navigation label for inbox"
  },
  "navSent": "Sent",
  "@navSent": {
    "description": "Navigation label for sent items"
  },
  "navDrafts": "Drafts",
  "@navDrafts": {
    "description": "Navigation label for drafts"
  },
  "navTrash": "Trash",
  "@navTrash": {
    "description": "Navigation label for trash"
  },
  "labelWork": "Work",
  "@labelWork": {
    "description": "Work label name"
  },
  "labelPersonal": "Personal",
  "@labelPersonal": {
    "description": "Personal label name"
  },
  "labelImportant": "Important",
  "@labelImportant": {
    "description": "Important label name"
  },
  "appName": "My App",
  "@appName": {
    "description": "Application name"
  }
}

Testing Navigation Localization

Write comprehensive tests:

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Navigation Localization Tests', () {
    testWidgets('displays localized navigation labels', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('en'),
          home: const LocalizedNavigationBar(),
        ),
      );

      expect(find.text('Home'), findsOneWidget);
      expect(find.text('Search'), findsOneWidget);
      expect(find.text('Favorites'), findsOneWidget);
      expect(find.text('Profile'), findsOneWidget);
    });

    testWidgets('shows badge with correct count', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('en'),
          home: const NavigationWithBadges(),
        ),
      );

      expect(find.text('5'), findsOneWidget); // Notification count
    });

    testWidgets('announces navigation changes for accessibility',
        (tester) async {
      final announcements = <String>[];

      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
        SystemChannels.accessibility,
        (message) {
          final data = message.arguments as Map;
          announcements.add(data['message'] as String);
          return Future.value();
        },
      );

      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('en'),
          home: const AccessibleNavigation(),
        ),
      );

      // Tap on Search tab
      await tester.tap(find.text('Explore'));
      await tester.pumpAndSettle();

      expect(announcements, contains('Navigated to Explore'));
    });

    testWidgets('shows correct destinations for user role', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('en'),
          home: const RoleBasedNavigation(userRole: UserRole.admin),
        ),
      );

      expect(find.text('Dashboard'), findsOneWidget);
      expect(find.text('Users'), findsOneWidget);
      expect(find.text('Admin'), findsOneWidget);
    });
  });
}

Best Practices Summary

  1. Concise labels: Keep navigation labels short (1-2 words)
  2. Meaningful tooltips: Provide additional context on hover
  3. Badge localization: Use proper pluralization for counts
  4. Accessibility: Announce navigation changes for screen readers
  5. RTL support: Handle directional icons appropriately
  6. Role-based navigation: Show relevant options per user type
  7. Consistent icons: Use outlined/filled pairs for selected state

Conclusion

Properly localized navigation is essential for usable multilingual Flutter apps. By following these patterns, you ensure all users can effectively navigate your app regardless of their language.

Key takeaways:

  • Localize all labels, tooltips, and accessibility text
  • Use proper pluralization for badge counts
  • Announce navigation changes for accessibility
  • Handle RTL layouts with appropriate icons
  • Test across all supported locales