← Back to Blog

Flutter AppBar Localization: Complete Guide to Titles, Actions, and Navigation

flutterappbarnavigationlocalizationsearchactions

Flutter AppBar Localization: Complete Guide to Titles, Actions, and Navigation

The AppBar is one of the most prominent UI elements in any Flutter application. It displays your app's title, navigation controls, and action buttons - all of which need proper localization for a global audience. This comprehensive guide covers everything you need to know about localizing AppBar components.

Understanding AppBar Components

Before diving into localization, let's identify all the localizable elements in a typical AppBar:

  • Title: The main heading text
  • Leading widget: Usually a back button or menu icon with accessibility labels
  • Actions: Icon buttons with tooltips
  • Search bar: Hint text, suggestions, and results
  • Flexible space: Any text in expanded areas

Basic AppBar Localization

Let's start with a simple localized AppBar:

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

class LocalizedAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String titleKey;
  final List<Widget>? actions;

  const LocalizedAppBar({
    super.key,
    required this.titleKey,
    this.actions,
  });

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

    return AppBar(
      title: Text(_getTitle(l10n)),
      actions: actions,
    );
  }

  String _getTitle(AppLocalizations l10n) {
    switch (titleKey) {
      case 'home':
        return l10n.homeTitle;
      case 'settings':
        return l10n.settingsTitle;
      case 'profile':
        return l10n.profileTitle;
      default:
        return titleKey;
    }
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

ARB Files for AppBar Localization

Here's a comprehensive ARB structure for AppBar elements:

English (app_en.arb)

{
  "@@locale": "en",

  "homeTitle": "Home",
  "@homeTitle": {
    "description": "Title for home screen"
  },

  "settingsTitle": "Settings",
  "@settingsTitle": {
    "description": "Title for settings screen"
  },

  "profileTitle": "Profile",
  "@profileTitle": {
    "description": "Title for profile screen"
  },

  "searchHint": "Search...",
  "@searchHint": {
    "description": "Hint text for search field"
  },

  "searchResultsCount": "{count, plural, =0{No results} =1{1 result} other{{count} results}}",
  "@searchResultsCount": {
    "description": "Number of search results",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  },

  "backButtonTooltip": "Go back",
  "@backButtonTooltip": {
    "description": "Tooltip for back button"
  },

  "menuButtonTooltip": "Open menu",
  "@menuButtonTooltip": {
    "description": "Tooltip for menu button"
  },

  "moreOptionsTooltip": "More options",
  "@moreOptionsTooltip": {
    "description": "Tooltip for more options button"
  },

  "shareTooltip": "Share",
  "@shareTooltip": {
    "description": "Tooltip for share button"
  },

  "notificationsTooltip": "Notifications",
  "@notificationsTooltip": {
    "description": "Tooltip for notifications button"
  },

  "notificationsBadge": "{count, plural, =0{No new notifications} =1{1 new notification} other{{count} new notifications}}",
  "@notificationsBadge": {
    "description": "Accessibility label for notification badge",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

Spanish (app_es.arb)

{
  "@@locale": "es",

  "homeTitle": "Inicio",
  "settingsTitle": "Configuracion",
  "profileTitle": "Perfil",
  "searchHint": "Buscar...",
  "searchResultsCount": "{count, plural, =0{Sin resultados} =1{1 resultado} other{{count} resultados}}",
  "backButtonTooltip": "Volver",
  "menuButtonTooltip": "Abrir menu",
  "moreOptionsTooltip": "Mas opciones",
  "shareTooltip": "Compartir",
  "notificationsTooltip": "Notificaciones",
  "notificationsBadge": "{count, plural, =0{Sin notificaciones nuevas} =1{1 notificacion nueva} other{{count} notificaciones nuevas}}"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "homeTitle": "الرئيسية",
  "settingsTitle": "الإعدادات",
  "profileTitle": "الملف الشخصي",
  "searchHint": "بحث...",
  "searchResultsCount": "{count, plural, =0{لا توجد نتائج} =1{نتيجة واحدة} two{نتيجتان} few{{count} نتائج} many{{count} نتيجة} other{{count} نتيجة}}",
  "backButtonTooltip": "رجوع",
  "menuButtonTooltip": "فتح القائمة",
  "moreOptionsTooltip": "خيارات إضافية",
  "shareTooltip": "مشاركة",
  "notificationsTooltip": "الإشعارات",
  "notificationsBadge": "{count, plural, =0{لا توجد إشعارات جديدة} =1{إشعار جديد واحد} two{إشعاران جديدان} few{{count} إشعارات جديدة} many{{count} إشعاراً جديداً} other{{count} إشعار جديد}}"
}

Localized Search AppBar

Implementing a fully localized search experience:

class LocalizedSearchAppBar extends StatefulWidget implements PreferredSizeWidget {
  final Function(String) onSearch;
  final VoidCallback? onClear;

  const LocalizedSearchAppBar({
    super.key,
    required this.onSearch,
    this.onClear,
  });

  @override
  State<LocalizedSearchAppBar> createState() => _LocalizedSearchAppBarState();

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

class _LocalizedSearchAppBarState extends State<LocalizedSearchAppBar> {
  final TextEditingController _controller = TextEditingController();
  bool _isSearching = false;

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

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

    return AppBar(
      leading: _isSearching
          ? IconButton(
              icon: Icon(isRtl ? Icons.arrow_forward : Icons.arrow_back),
              tooltip: l10n.backButtonTooltip,
              onPressed: () {
                setState(() {
                  _isSearching = false;
                  _controller.clear();
                });
                widget.onClear?.call();
              },
            )
          : null,
      title: _isSearching
          ? TextField(
              controller: _controller,
              autofocus: true,
              textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
              decoration: InputDecoration(
                hintText: l10n.searchHint,
                border: InputBorder.none,
                hintStyle: TextStyle(
                  color: Theme.of(context).colorScheme.onPrimary.withOpacity(0.7),
                ),
              ),
              style: TextStyle(
                color: Theme.of(context).colorScheme.onPrimary,
              ),
              onSubmitted: widget.onSearch,
            )
          : Text(l10n.homeTitle),
      actions: [
        if (!_isSearching)
          IconButton(
            icon: const Icon(Icons.search),
            tooltip: l10n.searchHint,
            onPressed: () {
              setState(() => _isSearching = true);
            },
          ),
        if (_isSearching && _controller.text.isNotEmpty)
          IconButton(
            icon: const Icon(Icons.clear),
            tooltip: l10n.clearSearch,
            onPressed: () {
              _controller.clear();
              widget.onClear?.call();
            },
          ),
      ],
    );
  }
}

AppBar with Localized Actions and Badges

class AppBarWithActions extends StatelessWidget implements PreferredSizeWidget {
  final int notificationCount;
  final VoidCallback onNotificationsTap;
  final VoidCallback onSettingsTap;
  final VoidCallback onShareTap;

  const AppBarWithActions({
    super.key,
    this.notificationCount = 0,
    required this.onNotificationsTap,
    required this.onSettingsTap,
    required this.onShareTap,
  });

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

    return AppBar(
      title: Text(l10n.homeTitle),
      actions: [
        // Share button
        IconButton(
          icon: const Icon(Icons.share),
          tooltip: l10n.shareTooltip,
          onPressed: onShareTap,
        ),

        // Notifications with badge
        Semantics(
          label: l10n.notificationsBadge(notificationCount),
          child: IconButton(
            icon: Badge(
              isLabelVisible: notificationCount > 0,
              label: Text(
                notificationCount > 99 ? '99+' : '$notificationCount',
              ),
              child: const Icon(Icons.notifications),
            ),
            tooltip: l10n.notificationsTooltip,
            onPressed: onNotificationsTap,
          ),
        ),

        // Settings/More options
        PopupMenuButton<String>(
          tooltip: l10n.moreOptionsTooltip,
          onSelected: (value) {
            if (value == 'settings') onSettingsTap();
          },
          itemBuilder: (context) => [
            PopupMenuItem(
              value: 'settings',
              child: ListTile(
                leading: const Icon(Icons.settings),
                title: Text(l10n.settingsTitle),
                contentPadding: EdgeInsets.zero,
              ),
            ),
          ],
        ),
      ],
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

SliverAppBar Localization

For apps using CustomScrollView with collapsible headers:

class LocalizedSliverAppBar extends StatelessWidget {
  final String title;
  final String? subtitle;
  final Widget? background;

  const LocalizedSliverAppBar({
    super.key,
    required this.title,
    this.subtitle,
    this.background,
  });

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

    return SliverAppBar(
      expandedHeight: 200.0,
      floating: false,
      pinned: true,
      leading: IconButton(
        icon: Icon(isRtl ? Icons.arrow_forward : Icons.arrow_back),
        tooltip: l10n.backButtonTooltip,
        onPressed: () => Navigator.of(context).pop(),
      ),
      flexibleSpace: FlexibleSpaceBar(
        title: Text(
          title,
          style: const TextStyle(
            shadows: [
              Shadow(
                offset: Offset(0, 1),
                blurRadius: 3.0,
                color: Colors.black45,
              ),
            ],
          ),
        ),
        centerTitle: true,
        background: background,
        collapseMode: CollapseMode.parallax,
      ),
      actions: [
        IconButton(
          icon: const Icon(Icons.share),
          tooltip: l10n.shareTooltip,
          onPressed: () {},
        ),
      ],
    );
  }
}

RTL Support for AppBar

Proper RTL handling is crucial for Arabic, Hebrew, and other RTL languages:

class RtlAwareAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String title;
  final bool showBackButton;
  final List<Widget>? actions;

  const RtlAwareAppBar({
    super.key,
    required this.title,
    this.showBackButton = true,
    this.actions,
  });

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

    return AppBar(
      automaticallyImplyLeading: false,
      leading: showBackButton
          ? IconButton(
              // Arrow direction flips automatically with Directionality
              icon: Icon(isRtl ? Icons.arrow_forward : Icons.arrow_back),
              tooltip: l10n.backButtonTooltip,
              onPressed: () => Navigator.of(context).pop(),
            )
          : null,
      title: Text(title),
      // Actions position automatically adjusts for RTL
      actions: actions,
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

Dynamic Title Based on Context

class ContextAwareAppBar extends StatelessWidget implements PreferredSizeWidget {
  final int selectedCount;
  final bool isSelectionMode;
  final VoidCallback onClearSelection;
  final VoidCallback onSelectAll;
  final VoidCallback onDelete;

  const ContextAwareAppBar({
    super.key,
    this.selectedCount = 0,
    this.isSelectionMode = false,
    required this.onClearSelection,
    required this.onSelectAll,
    required this.onDelete,
  });

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

    if (isSelectionMode) {
      return AppBar(
        leading: IconButton(
          icon: const Icon(Icons.close),
          tooltip: l10n.cancelSelection,
          onPressed: onClearSelection,
        ),
        title: Text(l10n.itemsSelected(selectedCount)),
        actions: [
          IconButton(
            icon: const Icon(Icons.select_all),
            tooltip: l10n.selectAll,
            onPressed: onSelectAll,
          ),
          IconButton(
            icon: const Icon(Icons.delete),
            tooltip: l10n.deleteSelected,
            onPressed: selectedCount > 0 ? onDelete : null,
          ),
        ],
      );
    }

    return AppBar(
      title: Text(l10n.homeTitle),
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

Accessibility Best Practices

Ensure your AppBar is accessible to all users:

class AccessibleAppBar extends StatelessWidget implements PreferredSizeWidget {
  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return AppBar(
      title: Semantics(
        header: true,
        child: Text(l10n.homeTitle),
      ),
      leading: Semantics(
        button: true,
        label: l10n.menuButtonTooltip,
        child: IconButton(
          icon: const Icon(Icons.menu),
          tooltip: l10n.menuButtonTooltip,
          onPressed: () => Scaffold.of(context).openDrawer(),
        ),
      ),
      actions: [
        Semantics(
          button: true,
          label: l10n.searchHint,
          child: IconButton(
            icon: const Icon(Icons.search),
            tooltip: l10n.searchHint,
            onPressed: () {},
          ),
        ),
      ],
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

Testing AppBar Localization

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('AppBar Localization Tests', () {
    testWidgets('displays localized title', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('es'),
          home: Scaffold(
            appBar: AppBar(
              title: Builder(
                builder: (context) => Text(
                  AppLocalizations.of(context)!.homeTitle,
                ),
              ),
            ),
          ),
        ),
      );

      expect(find.text('Inicio'), findsOneWidget);
    });

    testWidgets('tooltips are localized', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('es'),
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Test'),
              actions: [
                Builder(
                  builder: (context) => IconButton(
                    icon: const Icon(Icons.share),
                    tooltip: AppLocalizations.of(context)!.shareTooltip,
                    onPressed: () {},
                  ),
                ),
              ],
            ),
          ),
        ),
      );

      // Long press to show tooltip
      await tester.longPress(find.byIcon(Icons.share));
      await tester.pumpAndSettle();

      expect(find.text('Compartir'), findsOneWidget);
    });

    testWidgets('RTL layout is correct for Arabic', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('ar'),
          home: const Scaffold(
            appBar: RtlAwareAppBar(title: 'Test'),
          ),
        ),
      );

      final iconFinder = find.byIcon(Icons.arrow_forward);
      expect(iconFinder, findsOneWidget);
    });
  });
}

Best Practices Summary

  1. Always use tooltips: Every action button should have a localized tooltip for accessibility
  2. Handle RTL properly: Use Directionality.of(context) to determine text direction
  3. Use semantic labels: Add Semantics widgets for screen readers
  4. Localize search: Include hint text, results count, and suggestions
  5. Context-aware titles: Update titles based on selection mode or screen state
  6. Test all locales: Ensure text fits and displays correctly in all supported languages

Conclusion

Localizing the AppBar is essential for creating a truly international Flutter app. By following the patterns and practices in this guide, you can ensure your navigation header works seamlessly for users worldwide, regardless of their language or text direction preferences.

Remember to test your localized AppBar on actual devices and with real users who speak your target languages. This helps catch issues that automated testing might miss, such as text overflow or cultural appropriateness of icons.