Flutter BottomAppBar Localization: Bottom Toolbars for Multilingual Apps
BottomAppBar is a Flutter widget that displays a toolbar at the bottom of the screen, often paired with a FloatingActionButton embedded in a notch. In multilingual applications, BottomAppBar is essential for presenting translated action labels and tooltips in a bottom toolbar, positioning the FAB notch correctly in both LTR and RTL layouts, providing localized navigation actions alongside a primary action button, and building accessible bottom toolbars with translated semantic labels.
Understanding BottomAppBar in Localization Context
BottomAppBar renders a Material Design bottom toolbar that can contain icons, text, and a cutout for a FloatingActionButton. For multilingual apps, this enables:
- Translated icon button tooltips in the bottom toolbar
- RTL-aware icon ordering that reverses automatically
- Localized action labels for navigation and quick actions
- Accessible bottom bars with translated semantic descriptions
Why BottomAppBar Matters for Multilingual Apps
BottomAppBar provides:
- Flexible toolbar layout: Any combination of translated action buttons and labels
- FAB integration: A notch that positions correctly regardless of text direction
- Custom content: Unlike BottomNavigationBar, it allows free-form translated content
- Material 3 styling: Surface tint and elevation that work across themes
Basic BottomAppBar Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedBottomAppBarExample extends StatelessWidget {
const LocalizedBottomAppBarExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.messagesTitle)),
body: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text('${l10n.messageFromLabel} ${l10n.userLabel} ${index + 1}'),
subtitle: Text(l10n.messagePlaceholder),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: l10n.composeTooltip,
child: const Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
child: Row(
children: [
IconButton(
icon: const Icon(Icons.menu),
tooltip: l10n.menuTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.search),
tooltip: l10n.searchTooltip,
onPressed: () {},
),
const Spacer(),
IconButton(
icon: const Icon(Icons.archive_outlined),
tooltip: l10n.archiveTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: l10n.deleteTooltip,
onPressed: () {},
),
],
),
),
);
}
}
Advanced BottomAppBar Patterns for Localization
Contextual Actions with Translated Labels
A BottomAppBar that changes its actions based on selection state with translated tooltips.
class ContextualBottomAppBar extends StatefulWidget {
const ContextualBottomAppBar({super.key});
@override
State<ContextualBottomAppBar> createState() => _ContextualBottomAppBarState();
}
class _ContextualBottomAppBarState extends State<ContextualBottomAppBar> {
final Set<int> _selectedItems = {};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final hasSelection = _selectedItems.isNotEmpty;
return Scaffold(
appBar: AppBar(
title: hasSelection
? Text(l10n.selectedCountLabel(_selectedItems.length))
: Text(l10n.inboxTitle),
actions: hasSelection
? [
IconButton(
icon: const Icon(Icons.close),
tooltip: l10n.clearSelectionTooltip,
onPressed: () => setState(() => _selectedItems.clear()),
),
]
: null,
),
body: ListView.builder(
itemCount: 15,
itemBuilder: (context, index) {
final isSelected = _selectedItems.contains(index);
return ListTile(
selected: isSelected,
leading: isSelected
? const Icon(Icons.check_circle)
: const CircleAvatar(child: Icon(Icons.mail)),
title: Text('${l10n.emailSubjectLabel} ${index + 1}'),
subtitle: Text(l10n.emailPreviewText),
onLongPress: () {
setState(() {
if (isSelected) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
});
},
onTap: () {
if (hasSelection) {
setState(() {
if (isSelected) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
});
}
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: hasSelection
? l10n.moveSelectedTooltip
: l10n.composeTooltip,
child: Icon(hasSelection ? Icons.drive_file_move : Icons.edit),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
child: hasSelection
? Row(
children: [
IconButton(
icon: const Icon(Icons.archive_outlined),
tooltip: l10n.archiveSelectedTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.mark_email_read),
tooltip: l10n.markAsReadTooltip,
onPressed: () {},
),
const Spacer(),
IconButton(
icon: const Icon(Icons.label_outline),
tooltip: l10n.addLabelTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: l10n.deleteSelectedTooltip,
onPressed: () {},
),
],
)
: Row(
children: [
IconButton(
icon: const Icon(Icons.inbox),
tooltip: l10n.inboxTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.label_outline),
tooltip: l10n.labelsTooltip,
onPressed: () {},
),
const Spacer(),
IconButton(
icon: const Icon(Icons.search),
tooltip: l10n.searchTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.more_vert),
tooltip: l10n.moreOptionsTooltip,
onPressed: () {},
),
],
),
),
);
}
}
BottomAppBar with Text Labels
A BottomAppBar that displays translated text labels below icons for clarity.
class LabeledBottomAppBar extends StatelessWidget {
const LabeledBottomAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.dashboardTitle)),
body: const Center(child: Placeholder()),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: l10n.addNewTooltip,
child: const Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_BottomBarItem(
icon: Icons.home_outlined,
label: l10n.homeLabel,
onPressed: () {},
),
_BottomBarItem(
icon: Icons.analytics_outlined,
label: l10n.analyticsLabel,
onPressed: () {},
),
const SizedBox(width: 48),
_BottomBarItem(
icon: Icons.notifications_outlined,
label: l10n.notificationsLabel,
onPressed: () {},
),
_BottomBarItem(
icon: Icons.person_outline,
label: l10n.profileLabel,
onPressed: () {},
),
],
),
),
);
}
}
class _BottomBarItem extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onPressed;
const _BottomBarItem({
required this.icon,
required this.label,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 24),
const SizedBox(height: 4),
Text(
label,
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}
RTL Support and Bidirectional Layouts
BottomAppBar automatically reverses its children's order in RTL layouts when using a Row. The FAB notch position adjusts correctly based on FloatingActionButtonLocation.
class BidirectionalBottomAppBar extends StatelessWidget {
const BidirectionalBottomAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.filesTitle)),
body: const Center(child: Placeholder()),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: l10n.uploadTooltip,
child: const Icon(Icons.upload_file),
),
floatingActionButtonLocation: isRtl
? FloatingActionButtonLocation.startDocked
: FloatingActionButtonLocation.endDocked,
bottomNavigationBar: BottomAppBar(
child: Row(
children: [
IconButton(
icon: const Icon(Icons.folder_outlined),
tooltip: l10n.foldersTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.access_time),
tooltip: l10n.recentTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.star_outline),
tooltip: l10n.starredTooltip,
onPressed: () {},
),
],
),
),
);
}
}
Testing BottomAppBar Localization
import 'package:flutter/material.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 MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedBottomAppBarExample(),
);
}
testWidgets('BottomAppBar renders with localized tooltips', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(BottomAppBar), findsOneWidget);
});
testWidgets('BottomAppBar works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
Best Practices
Always provide translated
tooltipfor everyIconButtonin the BottomAppBar so screen readers announce the action in the active language.Use
Spacerbetween icon groups to separate leading and trailing actions, which naturally adapts to RTL by reversing the row.Adjust FAB location for RTL using
FloatingActionButtonLocation.startDockedorendDockedbased onDirectionality.of(context)when the FAB is not centered.Switch toolbar actions contextually with translated tooltips when selection state changes, showing relevant actions like "Archive selected" or "Delete selected".
Keep text labels short in BottomAppBar items since space is limited -- use
overflow: TextOverflow.ellipsisfor long translations.Test with verbose languages to verify tooltips display fully and icon buttons don't overflow the toolbar width.
Conclusion
BottomAppBar provides a flexible bottom toolbar for Flutter apps that pairs with a FloatingActionButton. For multilingual apps, it handles translated tooltips and action labels, supports RTL-aware icon ordering, and enables contextual actions with localized feedback. By combining BottomAppBar with selection-aware toolbars, text-labeled icons, and bidirectional FAB positioning, you can build bottom navigation experiences that work naturally across all supported languages.