← Back to Blog

Flutter CupertinoNavigationBar Localization: iOS-Style Navigation for Multilingual Apps

fluttercupertinonavigationioslocalizationrtl

Flutter CupertinoNavigationBar Localization: iOS-Style Navigation for Multilingual Apps

CupertinoNavigationBar is a Flutter widget that provides an iOS-style navigation bar with a centered title, leading/trailing actions, and automatic back button behavior. In multilingual applications, CupertinoNavigationBar is essential for displaying translated page titles that center correctly, providing localized back button labels, adapting navigation actions for RTL layouts, and maintaining iOS navigation conventions across all supported locales.

Understanding CupertinoNavigationBar in Localization Context

CupertinoNavigationBar renders a translucent bar at the top of a CupertinoPageScaffold with a centered title, optional leading widget (typically a back button), and trailing action buttons. For multilingual apps, this enables:

  • Centered translated titles that adapt to text length
  • Localized back button labels showing the previous page title
  • Trailing action buttons with translated labels
  • Large title mode with translated text that collapses on scroll

Why CupertinoNavigationBar Matters for Multilingual Apps

CupertinoNavigationBar provides:

  • Centered titles: Translated page titles that center between leading and trailing widgets
  • Back button labels: iOS-style back chevron with the previous page's translated title
  • Action buttons: Leading and trailing buttons with localized labels
  • Large titles: CupertinoSliverNavigationBar for large title mode with translated text

Basic CupertinoNavigationBar Implementation

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

class LocalizedCupertinoNavigationBarExample extends StatelessWidget {
  const LocalizedCupertinoNavigationBarExample({super.key});

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.homeTitle),
        trailing: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () {},
          child: Text(l10n.editButton),
        ),
      ),
      child: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CupertinoButton.filled(
                onPressed: () {
                  Navigator.push(
                    context,
                    CupertinoPageRoute(
                      title: l10n.detailsTitle,
                      builder: (_) => const _DetailPage(),
                    ),
                  );
                },
                child: Text(l10n.viewDetailsButton),
              ),
              const SizedBox(height: 16),
              CupertinoButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    CupertinoPageRoute(
                      title: l10n.settingsTitle,
                      builder: (_) => const _SettingsPage(),
                    ),
                  );
                },
                child: Text(l10n.settingsButton),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _DetailPage extends StatelessWidget {
  const _DetailPage();

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.detailsTitle),
        trailing: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () {},
          child: const Icon(CupertinoIcons.share),
        ),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(l10n.detailsContent),
        ),
      ),
    );
  }
}

class _SettingsPage extends StatelessWidget {
  const _SettingsPage();

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.settingsTitle),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(l10n.settingsContent),
        ),
      ),
    );
  }
}

Advanced CupertinoNavigationBar Patterns for Localization

Large Title Navigation with Localized Text

CupertinoSliverNavigationBar provides the iOS large title style that collapses on scroll, with translated titles.

class LocalizedLargeTitleNavigation extends StatelessWidget {
  const LocalizedLargeTitleNavigation({super.key});

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

    return CupertinoPageScaffold(
      child: CustomScrollView(
        slivers: [
          CupertinoSliverNavigationBar(
            largeTitle: Text(l10n.inboxTitle),
            trailing: CupertinoButton(
              padding: EdgeInsets.zero,
              onPressed: () {},
              child: const Icon(CupertinoIcons.compose),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                return CupertinoListTile(
                  title: Text('${l10n.messageLabel} ${index + 1}'),
                  subtitle: Text(l10n.messagePreview),
                  leading: const Icon(CupertinoIcons.mail),
                  trailing: const CupertinoListTileChevron(),
                  onTap: () {
                    Navigator.push(
                      context,
                      CupertinoPageRoute(
                        title: l10n.inboxTitle,
                        builder: (_) => _MessageDetailPage(index: index),
                      ),
                    );
                  },
                );
              },
              childCount: 20,
            ),
          ),
        ],
      ),
    );
  }
}

class _MessageDetailPage extends StatelessWidget {
  final int index;
  const _MessageDetailPage({required this.index});

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('${l10n.messageLabel} ${index + 1}'),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            CupertinoButton(
              padding: EdgeInsets.zero,
              onPressed: () {},
              child: const Icon(CupertinoIcons.reply),
            ),
            CupertinoButton(
              padding: EdgeInsets.zero,
              onPressed: () {},
              child: const Icon(CupertinoIcons.trash),
            ),
          ],
        ),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(l10n.messageContent),
        ),
      ),
    );
  }
}

Tab Navigation with Localized Tab Labels

CupertinoTabScaffold with CupertinoNavigationBar in each tab, all using translated titles.

class LocalizedCupertinoTabApp extends StatelessWidget {
  const LocalizedCupertinoTabApp({super.key});

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

    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: [
          BottomNavigationBarItem(
            icon: const Icon(CupertinoIcons.home),
            label: l10n.homeTab,
          ),
          BottomNavigationBarItem(
            icon: const Icon(CupertinoIcons.search),
            label: l10n.searchTab,
          ),
          BottomNavigationBarItem(
            icon: const Icon(CupertinoIcons.person),
            label: l10n.profileTab,
          ),
        ],
      ),
      tabBuilder: (context, index) {
        return CupertinoTabView(
          builder: (context) {
            switch (index) {
              case 0:
                return CupertinoPageScaffold(
                  navigationBar: CupertinoNavigationBar(
                    middle: Text(l10n.homeTitle),
                  ),
                  child: Center(child: Text(l10n.homeContent)),
                );
              case 1:
                return CupertinoPageScaffold(
                  child: CustomScrollView(
                    slivers: [
                      CupertinoSliverNavigationBar(
                        largeTitle: Text(l10n.searchTitle),
                      ),
                      SliverToBoxAdapter(
                        child: Padding(
                          padding: const EdgeInsets.all(8),
                          child: CupertinoSearchTextField(
                            placeholder: l10n.searchPlaceholder,
                          ),
                        ),
                      ),
                    ],
                  ),
                );
              case 2:
                return CupertinoPageScaffold(
                  navigationBar: CupertinoNavigationBar(
                    middle: Text(l10n.profileTitle),
                    trailing: CupertinoButton(
                      padding: EdgeInsets.zero,
                      onPressed: () {},
                      child: Text(l10n.editButton),
                    ),
                  ),
                  child: Center(child: Text(l10n.profileContent)),
                );
              default:
                return const SizedBox.shrink();
            }
          },
        );
      },
    );
  }
}

Navigation Bar with Localized Actions Menu

Navigation bar trailing area with multiple localized action buttons.

class NavigationBarWithActions extends StatelessWidget {
  const NavigationBarWithActions({super.key});

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.documentTitle),
        leading: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () => Navigator.maybePop(context),
          child: Text(l10n.cancelButton),
        ),
        trailing: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () {
            showCupertinoModalPopup(
              context: context,
              builder: (context) {
                final sheetL10n = AppLocalizations.of(context)!;
                return CupertinoActionSheet(
                  actions: [
                    CupertinoActionSheetAction(
                      onPressed: () => Navigator.pop(context),
                      child: Text(sheetL10n.saveButton),
                    ),
                    CupertinoActionSheetAction(
                      onPressed: () => Navigator.pop(context),
                      child: Text(sheetL10n.exportButton),
                    ),
                    CupertinoActionSheetAction(
                      onPressed: () => Navigator.pop(context),
                      child: Text(sheetL10n.printButton),
                    ),
                    CupertinoActionSheetAction(
                      isDestructiveAction: true,
                      onPressed: () => Navigator.pop(context),
                      child: Text(sheetL10n.deleteButton),
                    ),
                  ],
                  cancelButton: CupertinoActionSheetAction(
                    onPressed: () => Navigator.pop(context),
                    child: Text(sheetL10n.cancelButton),
                  ),
                );
              },
            );
          },
          child: const Icon(CupertinoIcons.ellipsis_circle),
        ),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(l10n.documentContent),
        ),
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

CupertinoNavigationBar automatically adapts to RTL. The back chevron flips direction, leading/trailing widgets swap positions, and the title remains centered.

class BidirectionalCupertinoNavigation extends StatelessWidget {
  const BidirectionalCupertinoNavigation({super.key});

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.settingsTitle),
        leading: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () => Navigator.maybePop(context),
          child: Text(l10n.doneButton),
        ),
        trailing: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () {},
          child: const Icon(CupertinoIcons.add),
        ),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsetsDirectional.all(16),
          child: Text(
            l10n.settingsContent,
            textAlign: TextAlign.start,
          ),
        ),
      ),
    );
  }
}

Testing CupertinoNavigationBar Localization

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

void main() {
  Widget buildTestWidget({Locale locale = const Locale('en')}) {
    return CupertinoApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedCupertinoNavigationBarExample(),
    );
  }

  testWidgets('CupertinoNavigationBar shows localized title', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(CupertinoNavigationBar), findsOneWidget);
  });

  testWidgets('Navigation works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Pass title to CupertinoPageRoute so the next page's back button automatically shows the previous page's translated title.

  2. Use CupertinoSliverNavigationBar for pages with scrollable content where the large translated title should collapse on scroll.

  3. Keep navigation bar titles concise -- iOS centers the title between leading and trailing widgets, so long translations may clip. Use Text with overflow handling.

  4. Use text buttons for trailing actions (Edit, Done, Save) with translated labels rather than icon-only buttons when the action needs clarification.

  5. Wrap multiple trailing actions in a Row with mainAxisSize: MainAxisSize.min to prevent layout overflow.

  6. Test back navigation in RTL to verify the back chevron flips direction and the previous page's translated title appears correctly.

Conclusion

CupertinoNavigationBar is the standard navigation element for iOS-style Flutter apps. For multilingual apps, it automatically centers translated titles, provides localized back button labels from CupertinoPageRoute, and adapts leading/trailing actions for RTL layouts. By combining CupertinoNavigationBar with large title mode, tab navigation, and localized action menus, you can build iOS navigation experiences that feel native and communicate clearly in every supported language.

Further Reading