Flutter GridView Localization: Adaptive Grids, Responsive Layouts, and Localized Content
GridView is essential for displaying collections of items in a two-dimensional scrollable grid. Localizing GridView requires handling item labels, responsive layouts for different text lengths, RTL support, and accessibility features. This guide covers everything you need to know about localizing GridView in Flutter.
Understanding GridView Localization
GridView requires localization for:
- Item titles and labels: Text displayed within grid items
- Category headers: Section titles above grid groups
- Empty state messages: What to show when no items exist
- Loading indicators: Progress text during data fetching
- Responsive layouts: Adapting to varying text lengths
- RTL support: Proper mirroring for right-to-left languages
Basic GridView with Localized Items
Start with a simple localized grid:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedGridView extends StatelessWidget {
const LocalizedGridView({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.productsTitle),
),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemCount: 12,
itemBuilder: (context, index) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Container(
width: double.infinity,
color: Colors.grey[200],
child: Icon(
Icons.image,
size: 48,
color: Colors.grey[400],
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.productName(index + 1),
style: Theme.of(context).textTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
l10n.productPrice(29.99 + index),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
],
),
),
),
],
),
);
},
),
);
}
}
Adaptive GridView Based on Locale
Adjust grid columns based on text length for different languages:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AdaptiveLocalizedGrid extends StatelessWidget {
const AdaptiveLocalizedGrid({super.key});
int _getColumnCount(BuildContext context) {
final locale = Localizations.localeOf(context);
final screenWidth = MediaQuery.of(context).size.width;
// Languages with longer text need fewer columns
final languagesWithLongText = ['de', 'ru', 'fi', 'hu'];
final isLongTextLocale = languagesWithLongText.contains(locale.languageCode);
if (screenWidth < 400) {
return isLongTextLocale ? 1 : 2;
} else if (screenWidth < 600) {
return isLongTextLocale ? 2 : 3;
} else {
return isLongTextLocale ? 3 : 4;
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final columnCount = _getColumnCount(context);
return Scaffold(
appBar: AppBar(
title: Text(l10n.categoriesTitle),
),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.2,
),
itemCount: 8,
itemBuilder: (context, index) {
return _buildCategoryTile(context, l10n, index);
},
),
);
}
Widget _buildCategoryTile(
BuildContext context,
AppLocalizations l10n,
int index,
) {
final categories = [
(l10n.categoryElectronics, Icons.devices),
(l10n.categoryClothing, Icons.checkroom),
(l10n.categoryHome, Icons.home),
(l10n.categorySports, Icons.sports_soccer),
(l10n.categoryBooks, Icons.book),
(l10n.categoryToys, Icons.toys),
(l10n.categoryBeauty, Icons.spa),
(l10n.categoryFood, Icons.restaurant),
];
final (title, icon) = categories[index];
return Card(
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 36, color: Theme.of(context).primaryColor),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}
GridView with Localized Categories and Sections
Group grid items with localized headers:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SectionedGridView extends StatelessWidget {
const SectionedGridView({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.shopTitle),
),
body: CustomScrollView(
slivers: [
_buildSectionHeader(context, l10n.featuredSection),
_buildGridSection(context, l10n, 4),
_buildSectionHeader(context, l10n.newArrivalsSection),
_buildGridSection(context, l10n, 6),
_buildSectionHeader(context, l10n.bestSellersSection),
_buildGridSection(context, l10n, 8),
],
),
);
}
Widget _buildSectionHeader(BuildContext context, String title) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {},
child: Text(AppLocalizations.of(context)!.seeAllButton),
),
],
),
),
);
}
Widget _buildGridSection(
BuildContext context,
AppLocalizations l10n,
int itemCount,
) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildProductCard(context, l10n, index),
childCount: itemCount,
),
),
);
}
Widget _buildProductCard(
BuildContext context,
AppLocalizations l10n,
int index,
) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Container(
width: double.infinity,
color: Colors.grey[200],
child: Stack(
children: [
Center(
child: Icon(
Icons.shopping_bag,
size: 48,
color: Colors.grey[400],
),
),
if (index % 3 == 0)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text(
l10n.saleLabel,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.productName(index + 1),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
l10n.productPrice(19.99 + index * 5),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Row(
children: [
Icon(Icons.star, size: 14, color: Colors.amber[700]),
const SizedBox(width: 4),
Text(
l10n.ratingValue(4.5),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
),
),
],
),
);
}
}
GridView with Empty and Loading States
Handle localized empty and loading states:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum GridState { loading, empty, loaded, error }
class StatefulGridView extends StatefulWidget {
const StatefulGridView({super.key});
@override
State<StatefulGridView> createState() => _StatefulGridViewState();
}
class _StatefulGridViewState extends State<StatefulGridView> {
GridState _state = GridState.loading;
List<int> _items = [];
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _state = GridState.loading);
await Future.delayed(const Duration(seconds: 2));
setState(() {
_items = List.generate(12, (i) => i);
_state = _items.isEmpty ? GridState.empty : GridState.loaded;
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.galleryTitle),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: l10n.refreshTooltip,
onPressed: _loadData,
),
],
),
body: _buildBody(l10n),
);
}
Widget _buildBody(AppLocalizations l10n) {
switch (_state) {
case GridState.loading:
return _buildLoadingState(l10n);
case GridState.empty:
return _buildEmptyState(l10n);
case GridState.error:
return _buildErrorState(l10n);
case GridState.loaded:
return _buildLoadedState(l10n);
}
}
Widget _buildLoadingState(AppLocalizations l10n) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
l10n.loadingItemsMessage,
style: TextStyle(color: Colors.grey[600]),
),
],
),
);
}
Widget _buildEmptyState(AppLocalizations l10n) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.grid_off,
size: 80,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
l10n.noItemsTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
l10n.noItemsMessage,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadData,
icon: const Icon(Icons.refresh),
label: Text(l10n.tryAgainButton),
),
],
),
);
}
Widget _buildErrorState(AppLocalizations l10n) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 80,
color: Colors.red[400],
),
const SizedBox(height: 16),
Text(
l10n.errorTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
l10n.errorLoadingMessage,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadData,
icon: const Icon(Icons.refresh),
label: Text(l10n.retryButton),
),
],
),
);
}
Widget _buildLoadedState(AppLocalizations l10n) {
return RefreshIndicator(
onRefresh: _loadData,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _items.length,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
l10n.itemNumber(index + 1),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
);
}
}
GridView with RTL Support
Handle right-to-left layouts properly:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RTLGridView extends StatelessWidget {
const RTLGridView({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.albumsTitle),
),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.85,
),
itemCount: 8,
itemBuilder: (context, index) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Container(
color: Colors.grey[300],
child: Stack(
children: [
Center(
child: Icon(
Icons.photo_album,
size: 48,
color: Colors.grey[500],
),
),
// Position badge based on text direction
Positioned(
top: 8,
left: isRTL ? null : 8,
right: isRTL ? 8 : null,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.photoCount(10 + index * 5),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
),
],
),
),
),
Expanded(
flex: 2,
child: Padding(
// Use directional padding
padding: const EdgeInsetsDirectional.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.albumName(index + 1),
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
l10n.albumDate,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
const Spacer(),
Row(
children: [
Icon(
Icons.favorite,
size: 16,
color: Colors.red[400],
),
const SizedBox(width: 4),
Text(
l10n.likesCount(25 + index * 3),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
),
),
],
),
);
},
),
);
}
}
Accessibility for GridView
Ensure proper accessibility labels:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AccessibleGridView extends StatelessWidget {
const AccessibleGridView({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.menuTitle),
),
body: Semantics(
label: l10n.menuGridAccessibilityLabel,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: 9,
itemBuilder: (context, index) {
return _buildAccessibleMenuItem(context, l10n, index);
},
),
),
);
}
Widget _buildAccessibleMenuItem(
BuildContext context,
AppLocalizations l10n,
int index,
) {
final menuItems = [
(l10n.menuHome, Icons.home, l10n.menuHomeDescription),
(l10n.menuProfile, Icons.person, l10n.menuProfileDescription),
(l10n.menuSettings, Icons.settings, l10n.menuSettingsDescription),
(l10n.menuMessages, Icons.mail, l10n.menuMessagesDescription),
(l10n.menuNotifications, Icons.notifications, l10n.menuNotificationsDescription),
(l10n.menuFavorites, Icons.favorite, l10n.menuFavoritesDescription),
(l10n.menuOrders, Icons.shopping_bag, l10n.menuOrdersDescription),
(l10n.menuHelp, Icons.help, l10n.menuHelpDescription),
(l10n.menuAbout, Icons.info, l10n.menuAboutDescription),
];
final (title, icon, description) = menuItems[index];
return Semantics(
button: true,
label: '$title. $description',
child: Card(
child: InkWell(
onTap: () {
// Announce action
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.navigatingTo(title)),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 32,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
);
}
}
GridView with Selection and Localized Actions
Handle selection with localized feedback:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SelectableGridView extends StatefulWidget {
const SelectableGridView({super.key});
@override
State<SelectableGridView> createState() => _SelectableGridViewState();
}
class _SelectableGridViewState extends State<SelectableGridView> {
final Set<int> _selectedItems = {};
bool _isSelectionMode = false;
void _toggleSelection(int index) {
setState(() {
if (_selectedItems.contains(index)) {
_selectedItems.remove(index);
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
} else {
_selectedItems.add(index);
}
});
}
void _startSelectionMode(int index) {
setState(() {
_isSelectionMode = true;
_selectedItems.add(index);
});
}
void _clearSelection() {
setState(() {
_selectedItems.clear();
_isSelectionMode = false;
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: _isSelectionMode
? Text(l10n.selectedCount(_selectedItems.length))
: Text(l10n.photosTitle),
leading: _isSelectionMode
? IconButton(
icon: const Icon(Icons.close),
tooltip: l10n.cancelSelectionTooltip,
onPressed: _clearSelection,
)
: null,
actions: _isSelectionMode
? [
IconButton(
icon: const Icon(Icons.share),
tooltip: l10n.shareTooltip,
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.sharingItems(_selectedItems.length),
),
),
);
},
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: l10n.deleteTooltip,
onPressed: () => _showDeleteDialog(context, l10n),
),
]
: null,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: 24,
itemBuilder: (context, index) {
final isSelected = _selectedItems.contains(index);
return GestureDetector(
onTap: () {
if (_isSelectionMode) {
_toggleSelection(index);
}
},
onLongPress: () {
if (!_isSelectionMode) {
_startSelectionMode(index);
}
},
child: Semantics(
label: _isSelectionMode
? isSelected
? l10n.photoSelectedLabel(index + 1)
: l10n.photoNotSelectedLabel(index + 1)
: l10n.photoLabel(index + 1),
child: Container(
decoration: BoxDecoration(
color: Colors.grey[300],
border: isSelected
? Border.all(
color: Theme.of(context).primaryColor,
width: 3,
)
: null,
),
child: Stack(
fit: StackFit.expand,
children: [
Icon(
Icons.photo,
size: 40,
color: Colors.grey[500],
),
if (_isSelectionMode)
Positioned(
top: 4,
right: 4,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor
: Colors.white.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).primaryColor,
width: 2,
),
),
child: isSelected
? const Icon(
Icons.check,
size: 16,
color: Colors.white,
)
: null,
),
),
],
),
),
),
);
},
),
floatingActionButton: _isSelectionMode
? null
: FloatingActionButton(
onPressed: () {},
tooltip: l10n.addPhotoTooltip,
child: const Icon(Icons.add_photo_alternate),
),
);
}
void _showDeleteDialog(BuildContext context, AppLocalizations l10n) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.deletePhotosTitle),
content: Text(
l10n.deletePhotosConfirmation(_selectedItems.length),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() {
_selectedItems.clear();
_isSelectionMode = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.photosDeletedMessage),
),
);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(l10n.deleteButton),
),
],
),
);
}
}
ARB Translations for GridView
Add these entries to your ARB files:
{
"productsTitle": "Products",
"@productsTitle": {
"description": "Title for products grid"
},
"productName": "Product {number}",
"@productName": {
"placeholders": {
"number": {"type": "int"}
}
},
"productPrice": "${price}",
"@productPrice": {
"placeholders": {
"price": {"type": "double", "format": "currency", "optionalParameters": {"symbol": "$"}}
}
},
"categoriesTitle": "Categories",
"categoryElectronics": "Electronics",
"categoryClothing": "Clothing",
"categoryHome": "Home & Garden",
"categorySports": "Sports",
"categoryBooks": "Books",
"categoryToys": "Toys",
"categoryBeauty": "Beauty",
"categoryFood": "Food & Drinks",
"shopTitle": "Shop",
"featuredSection": "Featured",
"newArrivalsSection": "New Arrivals",
"bestSellersSection": "Best Sellers",
"seeAllButton": "See All",
"saleLabel": "SALE",
"ratingValue": "{rating} stars",
"@ratingValue": {
"placeholders": {
"rating": {"type": "double"}
}
},
"galleryTitle": "Gallery",
"refreshTooltip": "Refresh",
"loadingItemsMessage": "Loading items...",
"noItemsTitle": "No Items Found",
"noItemsMessage": "There are no items to display. Try refreshing or adding new items.",
"tryAgainButton": "Try Again",
"errorTitle": "Something Went Wrong",
"errorLoadingMessage": "We couldn't load the items. Please check your connection and try again.",
"retryButton": "Retry",
"itemNumber": "Item {number}",
"@itemNumber": {
"placeholders": {
"number": {"type": "int"}
}
},
"albumsTitle": "Albums",
"photoCount": "{count} photos",
"@photoCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"albumName": "Album {number}",
"@albumName": {
"placeholders": {
"number": {"type": "int"}
}
},
"albumDate": "January 2024",
"likesCount": "{count} likes",
"@likesCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"menuTitle": "Menu",
"menuGridAccessibilityLabel": "Application menu grid with 9 options",
"menuHome": "Home",
"menuHomeDescription": "Go to the home screen",
"menuProfile": "Profile",
"menuProfileDescription": "View and edit your profile",
"menuSettings": "Settings",
"menuSettingsDescription": "App settings and preferences",
"menuMessages": "Messages",
"menuMessagesDescription": "View your messages",
"menuNotifications": "Notifications",
"menuNotificationsDescription": "View notification settings",
"menuFavorites": "Favorites",
"menuFavoritesDescription": "View saved favorites",
"menuOrders": "Orders",
"menuOrdersDescription": "View order history",
"menuHelp": "Help",
"menuHelpDescription": "Get help and support",
"menuAbout": "About",
"menuAboutDescription": "About this app",
"navigatingTo": "Navigating to {destination}",
"@navigatingTo": {
"placeholders": {
"destination": {"type": "String"}
}
},
"photosTitle": "Photos",
"selectedCount": "{count} selected",
"@selectedCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"cancelSelectionTooltip": "Cancel selection",
"shareTooltip": "Share",
"deleteTooltip": "Delete",
"sharingItems": "Sharing {count} items",
"@sharingItems": {
"placeholders": {
"count": {"type": "int"}
}
},
"photoLabel": "Photo {number}",
"@photoLabel": {
"placeholders": {
"number": {"type": "int"}
}
},
"photoSelectedLabel": "Photo {number}, selected",
"@photoSelectedLabel": {
"placeholders": {
"number": {"type": "int"}
}
},
"photoNotSelectedLabel": "Photo {number}, not selected",
"@photoNotSelectedLabel": {
"placeholders": {
"number": {"type": "int"}
}
},
"addPhotoTooltip": "Add photo",
"deletePhotosTitle": "Delete Photos",
"deletePhotosConfirmation": "Are you sure you want to delete {count} photos? This action cannot be undone.",
"@deletePhotosConfirmation": {
"placeholders": {
"count": {"type": "int"}
}
},
"cancelButton": "Cancel",
"deleteButton": "Delete",
"photosDeletedMessage": "Photos deleted successfully"
}
Testing GridView Localization
Write tests for your localized GridView:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
group('LocalizedGridView', () {
testWidgets('displays localized title in English', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const LocalizedGridView(),
),
);
expect(find.text('Products'), findsOneWidget);
});
testWidgets('displays localized title in Spanish', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('es'),
home: const LocalizedGridView(),
),
);
expect(find.text('Productos'), findsOneWidget);
});
testWidgets('shows loading state with localized message', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const StatefulGridView(),
),
);
expect(find.text('Loading items...'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('handles RTL layout correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('ar'),
home: const Directionality(
textDirection: TextDirection.rtl,
child: RTLGridView(),
),
),
);
// Verify RTL text is displayed
expect(find.text('الألبومات'), findsOneWidget);
});
testWidgets('selection mode shows localized count', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const SelectableGridView(),
),
);
// Long press to enter selection mode
await tester.longPress(find.byType(Container).first);
await tester.pumpAndSettle();
expect(find.text('1 selected'), findsOneWidget);
});
});
}
Summary
Localizing GridView in Flutter requires:
- Localized item content with proper text handling
- Adaptive layouts that adjust columns for different text lengths
- Sectioned grids with translated headers
- Empty and loading states with appropriate messages
- RTL support with directional positioning
- Accessibility labels for screen reader users
- Selection feedback in the user's language
- Comprehensive testing across different locales
GridView is fundamental for displaying collections, and proper localization ensures your app looks polished and feels natural for users in any language.