Flutter DraggableScrollableSheet Localization: Modal Sheets, Drag Handles, and Expandable Content
DraggableScrollableSheet provides expandable bottom sheets that users can drag to reveal more content. Localizing these sheets requires handling drag handle instructions, content headers, action buttons, and accessibility announcements across different languages. This guide covers everything you need to know about localizing DraggableScrollableSheet in Flutter.
Understanding DraggableScrollableSheet Localization
DraggableScrollableSheet requires localization for:
- Drag handle labels: Instructions for expanding/collapsing
- Sheet headers: Titles and subtitles
- Content sections: Lists, forms, and details
- Action buttons: Primary and secondary actions
- Accessibility: Screen reader announcements for drag states
- RTL support: Proper layout for Arabic, Hebrew, etc.
Basic DraggableScrollableSheet with Localized Content
Start with a simple localized expandable sheet:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedDraggableSheet extends StatelessWidget {
const LocalizedDraggableSheet({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.mapViewTitle),
),
body: Stack(
children: [
// Main content (e.g., map)
Container(
color: Colors.grey[200],
child: Center(
child: Text(l10n.mapPlaceholder),
),
),
// Draggable sheet
DraggableScrollableSheet(
initialChildSize: 0.3,
minChildSize: 0.1,
maxChildSize: 0.9,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: ListView(
controller: scrollController,
padding: EdgeInsets.zero,
children: [
// Drag handle
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
),
),
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
l10n.nearbyPlacesTitle,
style: Theme.of(context).textTheme.titleLarge,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Text(
l10n.nearbyPlacesSubtitle,
style: TextStyle(color: Colors.grey[600]),
),
),
const Divider(),
// Location items
...List.generate(
10,
(index) => ListTile(
leading: CircleAvatar(
child: Icon(_getPlaceIcon(index)),
),
title: Text(l10n.placeName(index + 1)),
subtitle: Text(l10n.placeDistance(0.5 + index * 0.3)),
trailing: IconButton(
icon: const Icon(Icons.directions),
tooltip: l10n.getDirectionsTooltip,
onPressed: () {},
),
),
),
],
),
);
},
),
],
),
);
}
IconData _getPlaceIcon(int index) {
final icons = [
Icons.restaurant,
Icons.local_cafe,
Icons.shopping_bag,
Icons.local_gas_station,
Icons.local_pharmacy,
];
return icons[index % icons.length];
}
}
Product Details Sheet with Localization
Create a product details sheet with localized content:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ProductDetailsSheet extends StatelessWidget {
final String productId;
final VoidCallback onAddToCart;
const ProductDetailsSheet({
super.key,
required this.productId,
required this.onAddToCart,
});
static Future<void> show(BuildContext context, String productId) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ProductDetailsSheet(
productId: productId,
onAddToCart: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.addedToCartMessage),
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.95,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: Column(
children: [
// Drag indicator with semantic label
Semantics(
label: l10n.dragToExpandLabel,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
),
// Scrollable content
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
// Product image placeholder
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Icon(
Icons.image,
size: 64,
color: Colors.grey[500],
),
),
),
const SizedBox(height: 16),
// Product name
Text(
l10n.sampleProductName,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
// Price and rating row
Row(
children: [
Text(
l10n.productPrice(99.99),
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 20),
const SizedBox(width: 4),
Text(l10n.ratingValue(4.5)),
Text(
l10n.reviewCount(128),
style: TextStyle(color: Colors.grey[600]),
),
],
),
],
),
const SizedBox(height: 16),
// Description section
Text(
l10n.descriptionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
l10n.sampleProductDescription,
style: TextStyle(color: Colors.grey[700]),
),
const SizedBox(height: 16),
// Specifications
Text(
l10n.specificationsTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_buildSpecRow(l10n.specBrand, 'Premium Brand'),
_buildSpecRow(l10n.specMaterial, l10n.materialCotton),
_buildSpecRow(l10n.specColor, l10n.colorBlue),
_buildSpecRow(l10n.specSize, 'M, L, XL'),
const SizedBox(height: 24),
// Quantity selector
Row(
children: [
Text(
l10n.quantityLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
_QuantitySelector(l10n: l10n),
],
),
const SizedBox(height: 100), // Space for button
],
),
),
// Fixed bottom button
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.favorite_border),
label: Text(l10n.addToWishlistButton),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: onAddToCart,
icon: const Icon(Icons.shopping_cart),
label: Text(l10n.addToCartButton),
),
),
],
),
),
),
],
),
);
},
);
}
Widget _buildSpecRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Text(
label,
style: const TextStyle(color: Colors.grey),
),
const Spacer(),
Text(value),
],
),
);
}
}
class _QuantitySelector extends StatefulWidget {
final AppLocalizations l10n;
const _QuantitySelector({required this.l10n});
@override
State<_QuantitySelector> createState() => _QuantitySelectorState();
}
class _QuantitySelectorState extends State<_QuantitySelector> {
int _quantity = 1;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: _quantity > 1
? () => setState(() => _quantity--)
: null,
tooltip: widget.l10n.decreaseQuantityTooltip,
),
Semantics(
label: widget.l10n.quantityAccessibilityLabel(_quantity),
child: Text(
'$_quantity',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => setState(() => _quantity++),
tooltip: widget.l10n.increaseQuantityTooltip,
),
],
);
}
}
Filter Sheet with Multiple Sections
Create a localized filter sheet:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class FilterSheet extends StatefulWidget {
final Function(FilterOptions) onApply;
const FilterSheet({super.key, required this.onApply});
static Future<void> show(
BuildContext context,
Function(FilterOptions) onApply,
) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => FilterSheet(onApply: onApply),
);
}
@override
State<FilterSheet> createState() => _FilterSheetState();
}
class _FilterSheetState extends State<FilterSheet> {
RangeValues _priceRange = const RangeValues(0, 500);
String _selectedCategory = 'all';
double _minRating = 0;
bool _inStockOnly = false;
bool _onSaleOnly = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey[300]!),
),
),
child: Row(
children: [
Text(
l10n.filterTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
TextButton(
onPressed: _resetFilters,
child: Text(l10n.resetFiltersButton),
),
IconButton(
icon: const Icon(Icons.close),
tooltip: l10n.closeSheetTooltip,
onPressed: () => Navigator.pop(context),
),
],
),
),
// Filter content
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.all(16),
children: [
// Price range
Text(
l10n.priceRangeLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(l10n.priceValue(_priceRange.start.round())),
Text(l10n.priceValue(_priceRange.end.round())),
],
),
RangeSlider(
values: _priceRange,
min: 0,
max: 1000,
divisions: 20,
labels: RangeLabels(
l10n.priceValue(_priceRange.start.round()),
l10n.priceValue(_priceRange.end.round()),
),
onChanged: (values) {
setState(() => _priceRange = values);
},
),
const SizedBox(height: 24),
// Category
Text(
l10n.categoryLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildCategoryChip('all', l10n.categoryAll),
_buildCategoryChip('electronics', l10n.categoryElectronics),
_buildCategoryChip('clothing', l10n.categoryClothing),
_buildCategoryChip('home', l10n.categoryHome),
_buildCategoryChip('sports', l10n.categorySports),
],
),
const SizedBox(height: 24),
// Rating
Text(
l10n.minimumRatingLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
children: List.generate(5, (index) {
final rating = index + 1;
return Expanded(
child: GestureDetector(
onTap: () {
setState(() => _minRating = rating.toDouble());
},
child: Column(
children: [
Icon(
rating <= _minRating
? Icons.star
: Icons.star_border,
color: Colors.amber,
size: 32,
),
Text(
l10n.starRating(rating),
style: TextStyle(
fontSize: 12,
color: rating <= _minRating
? Colors.black
: Colors.grey,
),
),
],
),
),
);
}),
),
const SizedBox(height: 24),
// Toggle options
Text(
l10n.additionalOptionsLabel,
style: Theme.of(context).textTheme.titleMedium,
),
SwitchListTile(
title: Text(l10n.inStockOnlyLabel),
subtitle: Text(l10n.inStockOnlyDescription),
value: _inStockOnly,
onChanged: (value) {
setState(() => _inStockOnly = value);
},
),
SwitchListTile(
title: Text(l10n.onSaleOnlyLabel),
subtitle: Text(l10n.onSaleOnlyDescription),
value: _onSaleOnly,
onChanged: (value) {
setState(() => _onSaleOnly = value);
},
),
const SizedBox(height: 80),
],
),
),
// Apply button
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
widget.onApply(FilterOptions(
priceRange: _priceRange,
category: _selectedCategory,
minRating: _minRating,
inStockOnly: _inStockOnly,
onSaleOnly: _onSaleOnly,
));
Navigator.pop(context);
},
child: Text(l10n.applyFiltersButton),
),
),
),
),
],
),
);
},
);
}
Widget _buildCategoryChip(String value, String label) {
final isSelected = _selectedCategory == value;
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) {
setState(() => _selectedCategory = value);
},
);
}
void _resetFilters() {
setState(() {
_priceRange = const RangeValues(0, 500);
_selectedCategory = 'all';
_minRating = 0;
_inStockOnly = false;
_onSaleOnly = false;
});
}
}
class FilterOptions {
final RangeValues priceRange;
final String category;
final double minRating;
final bool inStockOnly;
final bool onSaleOnly;
FilterOptions({
required this.priceRange,
required this.category,
required this.minRating,
required this.inStockOnly,
required this.onSaleOnly,
});
}
Accessibility for DraggableScrollableSheet
Ensure sheets work well with screen readers:
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AccessibleDraggableSheet extends StatefulWidget {
const AccessibleDraggableSheet({super.key});
@override
State<AccessibleDraggableSheet> createState() =>
_AccessibleDraggableSheetState();
}
class _AccessibleDraggableSheetState extends State<AccessibleDraggableSheet> {
final DraggableScrollableController _controller =
DraggableScrollableController();
double _currentSize = 0.3;
@override
void initState() {
super.initState();
_controller.addListener(_onSizeChanged);
}
@override
void dispose() {
_controller.removeListener(_onSizeChanged);
_controller.dispose();
super.dispose();
}
void _onSizeChanged() {
final newSize = _controller.size;
if ((newSize - _currentSize).abs() > 0.1) {
_currentSize = newSize;
_announceState(context);
}
}
void _announceState(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
String announcement;
if (_currentSize > 0.8) {
announcement = l10n.sheetFullyExpandedAnnouncement;
} else if (_currentSize > 0.5) {
announcement = l10n.sheetPartiallyExpandedAnnouncement;
} else {
announcement = l10n.sheetCollapsedAnnouncement;
}
SemanticsService.announce(announcement, TextDirection.ltr);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.detailsTitle)),
body: Stack(
children: [
Container(
color: Colors.grey[100],
child: Center(child: Text(l10n.mainContentPlaceholder)),
),
DraggableScrollableSheet(
controller: _controller,
initialChildSize: 0.3,
minChildSize: 0.1,
maxChildSize: 0.9,
builder: (context, scrollController) {
return Semantics(
label: l10n.draggableSheetLabel,
hint: l10n.draggableSheetHint,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
),
],
),
child: Column(
children: [
// Accessible drag handle
Semantics(
button: true,
label: l10n.dragHandleLabel,
hint: l10n.dragHandleHint,
onTap: () => _toggleSheet(),
child: GestureDetector(
onTap: _toggleSheet,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
),
),
),
),
// Expand/collapse button for accessibility
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text(
l10n.contentTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
IconButton(
icon: Icon(
_currentSize > 0.5
? Icons.expand_more
: Icons.expand_less,
),
tooltip: _currentSize > 0.5
? l10n.collapseSheetTooltip
: l10n.expandSheetTooltip,
onPressed: _toggleSheet,
),
],
),
),
const Divider(),
// Content
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: 20,
itemBuilder: (context, index) => Semantics(
label: l10n.listItemAccessibilityLabel(index + 1),
child: ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text(l10n.listItemTitle(index + 1)),
subtitle: Text(l10n.listItemSubtitle),
),
),
),
),
],
),
),
);
},
),
],
),
);
}
void _toggleSheet() {
final l10n = AppLocalizations.of(context)!;
if (_currentSize > 0.5) {
_controller.animateTo(
0.3,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
SemanticsService.announce(
l10n.sheetCollapsingAnnouncement,
TextDirection.ltr,
);
} else {
_controller.animateTo(
0.9,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
SemanticsService.announce(
l10n.sheetExpandingAnnouncement,
TextDirection.ltr,
);
}
}
}
RTL Support for DraggableScrollableSheet
Handle right-to-left layouts:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RTLDraggableSheet extends StatelessWidget {
const RTLDraggableSheet({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.ordersTitle)),
body: Stack(
children: [
Container(color: Colors.grey[100]),
DraggableScrollableSheet(
initialChildSize: 0.4,
minChildSize: 0.2,
maxChildSize: 0.9,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: ListView(
controller: scrollController,
children: [
// Drag handle
Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
),
// Header with directional padding
Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 16,
),
child: Row(
children: [
Text(
l10n.recentOrdersTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
TextButton(
onPressed: () {},
child: Text(l10n.viewAllButton),
),
],
),
),
const Divider(),
// Order items with RTL-aware layout
...List.generate(5, (index) {
return Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 16,
vertical: 8,
),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
l10n.orderNumber(1000 + index),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green[100],
borderRadius: BorderRadius.circular(4),
),
child: Text(
l10n.orderStatusDelivered,
style: TextStyle(
color: Colors.green[800],
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Text(
l10n.orderDate('Jan ${10 + index}, 2026'),
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 4),
Row(
children: [
Text(
l10n.orderTotal(99.99 + index * 10),
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
// Arrow direction changes with RTL
TextButton.icon(
onPressed: () {},
icon: Icon(
isRTL
? Icons.arrow_back_ios
: Icons.arrow_forward_ios,
size: 14,
),
label: Text(l10n.viewDetailsButton),
),
],
),
],
),
),
),
);
}),
],
),
);
},
),
],
),
);
}
}
ARB Translations for DraggableScrollableSheet
Add these entries to your ARB files:
{
"mapViewTitle": "Map",
"mapPlaceholder": "Map View",
"nearbyPlacesTitle": "Nearby Places",
"nearbyPlacesSubtitle": "Discover places around you",
"placeName": "Place {number}",
"@placeName": {
"placeholders": {
"number": {"type": "int"}
}
},
"placeDistance": "{distance} km away",
"@placeDistance": {
"placeholders": {
"distance": {"type": "double", "format": "decimalPattern"}
}
},
"getDirectionsTooltip": "Get directions",
"dragToExpandLabel": "Drag to expand sheet",
"sampleProductName": "Premium Cotton T-Shirt",
"productPrice": "${price}",
"@productPrice": {
"placeholders": {
"price": {"type": "double", "format": "currency", "optionalParameters": {"symbol": "$"}}
}
},
"ratingValue": "{rating}",
"@ratingValue": {
"placeholders": {
"rating": {"type": "double", "format": "decimalPattern"}
}
},
"reviewCount": " ({count} reviews)",
"@reviewCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"descriptionTitle": "Description",
"sampleProductDescription": "High-quality premium cotton t-shirt with a comfortable fit. Perfect for everyday wear.",
"specificationsTitle": "Specifications",
"specBrand": "Brand",
"specMaterial": "Material",
"specColor": "Color",
"specSize": "Available Sizes",
"materialCotton": "100% Cotton",
"colorBlue": "Navy Blue",
"quantityLabel": "Quantity",
"decreaseQuantityTooltip": "Decrease quantity",
"increaseQuantityTooltip": "Increase quantity",
"quantityAccessibilityLabel": "Quantity: {count}",
"@quantityAccessibilityLabel": {
"placeholders": {
"count": {"type": "int"}
}
},
"addToWishlistButton": "Wishlist",
"addToCartButton": "Add to Cart",
"addedToCartMessage": "Added to cart",
"filterTitle": "Filters",
"resetFiltersButton": "Reset",
"closeSheetTooltip": "Close",
"priceRangeLabel": "Price Range",
"priceValue": "${value}",
"@priceValue": {
"placeholders": {
"value": {"type": "int"}
}
},
"categoryLabel": "Category",
"categoryAll": "All",
"categoryElectronics": "Electronics",
"categoryClothing": "Clothing",
"categoryHome": "Home",
"categorySports": "Sports",
"minimumRatingLabel": "Minimum Rating",
"starRating": "{stars}+",
"@starRating": {
"placeholders": {
"stars": {"type": "int"}
}
},
"additionalOptionsLabel": "Additional Options",
"inStockOnlyLabel": "In Stock Only",
"inStockOnlyDescription": "Show only items currently in stock",
"onSaleOnlyLabel": "On Sale Only",
"onSaleOnlyDescription": "Show only discounted items",
"applyFiltersButton": "Apply Filters",
"detailsTitle": "Details",
"mainContentPlaceholder": "Main Content",
"draggableSheetLabel": "Expandable details panel",
"draggableSheetHint": "Swipe up to expand, swipe down to collapse",
"dragHandleLabel": "Sheet drag handle",
"dragHandleHint": "Double tap to toggle expansion",
"contentTitle": "Content",
"collapseSheetTooltip": "Collapse sheet",
"expandSheetTooltip": "Expand sheet",
"listItemAccessibilityLabel": "List item {number}",
"@listItemAccessibilityLabel": {
"placeholders": {
"number": {"type": "int"}
}
},
"listItemTitle": "Item {number}",
"@listItemTitle": {
"placeholders": {
"number": {"type": "int"}
}
},
"listItemSubtitle": "Tap to view details",
"sheetFullyExpandedAnnouncement": "Sheet fully expanded",
"sheetPartiallyExpandedAnnouncement": "Sheet partially expanded",
"sheetCollapsedAnnouncement": "Sheet collapsed",
"sheetExpandingAnnouncement": "Expanding sheet",
"sheetCollapsingAnnouncement": "Collapsing sheet",
"ordersTitle": "Orders",
"recentOrdersTitle": "Recent Orders",
"viewAllButton": "View All",
"orderNumber": "Order #{number}",
"@orderNumber": {
"placeholders": {
"number": {"type": "int"}
}
},
"orderStatusDelivered": "Delivered",
"orderDate": "{date}",
"@orderDate": {
"placeholders": {
"date": {"type": "String"}
}
},
"orderTotal": "${total}",
"@orderTotal": {
"placeholders": {
"total": {"type": "double", "format": "currency", "optionalParameters": {"symbol": "$"}}
}
},
"viewDetailsButton": "Details"
}
Testing DraggableScrollableSheet Localization
Write tests for your localized sheets:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
group('LocalizedDraggableSheet', () {
testWidgets('displays localized content in English', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const LocalizedDraggableSheet(),
),
);
expect(find.text('Nearby Places'), findsOneWidget);
expect(find.text('Discover places around you'), findsOneWidget);
});
testWidgets('displays localized content in Spanish', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('es'),
home: const LocalizedDraggableSheet(),
),
);
expect(find.text('Lugares Cercanos'), findsOneWidget);
expect(find.text('Descubre lugares a tu alrededor'), findsOneWidget);
});
testWidgets('filter sheet displays localized options', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () => FilterSheet.show(context, (_) {}),
child: const Text('Open Filter'),
),
),
),
),
);
await tester.tap(find.text('Open Filter'));
await tester.pumpAndSettle();
expect(find.text('Filters'), findsOneWidget);
expect(find.text('Price Range'), findsOneWidget);
expect(find.text('Category'), findsOneWidget);
expect(find.text('Apply Filters'), 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: RTLDraggableSheet(),
),
),
);
expect(find.text('الطلبات'), findsOneWidget);
});
});
}
Summary
Localizing DraggableScrollableSheet in Flutter requires:
- Drag handle labels with accessibility hints
- Header translations for sheet titles and subtitles
- Content localization for lists and forms
- Action button labels for primary and secondary actions
- Accessibility announcements for drag state changes
- RTL support with directional padding and icons
- Filter and form labels in modal sheets
- Comprehensive testing across different locales
DraggableScrollableSheet provides flexible bottom sheets, and proper localization ensures your app delivers a polished experience for users in any language.