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
Pass
titletoCupertinoPageRouteso the next page's back button automatically shows the previous page's translated title.Use
CupertinoSliverNavigationBarfor pages with scrollable content where the large translated title should collapse on scroll.Keep navigation bar titles concise -- iOS centers the title between leading and trailing widgets, so long translations may clip. Use
Textwith overflow handling.Use text buttons for trailing actions (Edit, Done, Save) with translated labels rather than icon-only buttons when the action needs clarification.
Wrap multiple trailing actions in a
RowwithmainAxisSize: MainAxisSize.minto prevent layout overflow.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.