← Back to Blog

Flutter FutureBuilder Localization: Async Loading States, Error Messages, and Data Display

flutterfuturebuilderasyncloadinglocalizationaccessibility

Flutter FutureBuilder Localization: Async Loading States, Error Messages, and Data Display

FutureBuilder is essential for handling asynchronous operations in Flutter apps. Proper localization ensures loading indicators, error messages, and data presentations work seamlessly across languages. This guide covers comprehensive strategies for localizing FutureBuilder widgets in Flutter.

Understanding FutureBuilder Localization

FutureBuilder widgets require localization for:

  • Loading states: Progress indicators and loading text
  • Error messages: Network errors, timeouts, validation failures
  • Empty states: No data available messages
  • Success states: Data labels, formatted values, units
  • Retry buttons: Action text for failed requests
  • Accessibility: Screen reader announcements for state changes

Basic FutureBuilder with Localized States

Start with a simple FutureBuilder that handles all states:

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

class LocalizedUserProfile extends StatefulWidget {
  final String userId;

  const LocalizedUserProfile({super.key, required this.userId});

  @override
  State<LocalizedUserProfile> createState() => _LocalizedUserProfileState();
}

class _LocalizedUserProfileState extends State<LocalizedUserProfile> {
  late Future<User> _userFuture;

  @override
  void initState() {
    super.initState();
    _userFuture = _fetchUser();
  }

  Future<User> _fetchUser() async {
    await Future.delayed(const Duration(seconds: 2));
    // Simulate API call
    return User(name: 'John Doe', email: 'john@example.com');
  }

  void _retry() {
    setState(() {
      _userFuture = _fetchUser();
    });
  }

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

    return FutureBuilder<User>(
      future: _userFuture,
      builder: (context, snapshot) {
        // Loading state
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildLoadingState(l10n);
        }

        // Error state
        if (snapshot.hasError) {
          return _buildErrorState(l10n, snapshot.error!);
        }

        // No data state
        if (!snapshot.hasData) {
          return _buildEmptyState(l10n);
        }

        // Success state
        return _buildSuccessState(l10n, snapshot.data!);
      },
    );
  }

  Widget _buildLoadingState(AppLocalizations l10n) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const CircularProgressIndicator(),
          const SizedBox(height: 16),
          Text(
            l10n.loadingProfile,
            style: Theme.of(context).textTheme.bodyLarge,
          ),
        ],
      ),
    );
  }

  Widget _buildErrorState(AppLocalizations l10n, Object error) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Theme.of(context).colorScheme.error,
            ),
            const SizedBox(height: 16),
            Text(
              l10n.errorLoadingProfile,
              style: Theme.of(context).textTheme.titleLarge,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              _getLocalizedErrorMessage(l10n, error),
              style: Theme.of(context).textTheme.bodyMedium,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 24),
            ElevatedButton.icon(
              onPressed: _retry,
              icon: const Icon(Icons.refresh),
              label: Text(l10n.retryButton),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildEmptyState(AppLocalizations l10n) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.person_off,
            size: 64,
            color: Theme.of(context).colorScheme.outline,
          ),
          const SizedBox(height: 16),
          Text(
            l10n.profileNotFound,
            style: Theme.of(context).textTheme.titleLarge,
          ),
        ],
      ),
    );
  }

  Widget _buildSuccessState(AppLocalizations l10n, User user) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            l10n.profileTitle,
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 24),
          _buildInfoRow(l10n.nameLabel, user.name),
          _buildInfoRow(l10n.emailLabel, user.email),
        ],
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 120,
            child: Text(
              label,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }

  String _getLocalizedErrorMessage(AppLocalizations l10n, Object error) {
    if (error is NetworkException) {
      return l10n.networkError;
    } else if (error is TimeoutException) {
      return l10n.timeoutError;
    } else if (error is UnauthorizedException) {
      return l10n.unauthorizedError;
    }
    return l10n.unknownError;
  }
}

class User {
  final String name;
  final String email;

  User({required this.name, required this.email});
}

ARB File Structure for FutureBuilder

{
  "loadingProfile": "Loading profile...",
  "@loadingProfile": {
    "description": "Message shown while loading user profile"
  },
  "errorLoadingProfile": "Failed to load profile",
  "@errorLoadingProfile": {
    "description": "Error title when profile fails to load"
  },
  "profileNotFound": "Profile not found",
  "@profileNotFound": {
    "description": "Message when no profile data exists"
  },
  "profileTitle": "Profile Information",
  "@profileTitle": {
    "description": "Title for profile section"
  },
  "nameLabel": "Name:",
  "@nameLabel": {
    "description": "Label for name field"
  },
  "emailLabel": "Email:",
  "@emailLabel": {
    "description": "Label for email field"
  },
  "retryButton": "Try Again",
  "@retryButton": {
    "description": "Button text to retry failed operation"
  },
  "networkError": "Please check your internet connection and try again.",
  "@networkError": {
    "description": "Error message for network issues"
  },
  "timeoutError": "The request took too long. Please try again.",
  "@timeoutError": {
    "description": "Error message for timeout"
  },
  "unauthorizedError": "Your session has expired. Please log in again.",
  "@unauthorizedError": {
    "description": "Error message for authentication issues"
  },
  "unknownError": "Something went wrong. Please try again later.",
  "@unknownError": {
    "description": "Generic error message"
  }
}

Advanced: Typed Error Handling with Localization

Create a robust error handling system with localized messages:

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

// Custom exception types
abstract class AppException implements Exception {
  String getLocalizedMessage(AppLocalizations l10n);
}

class NetworkException extends AppException {
  final int? statusCode;

  NetworkException({this.statusCode});

  @override
  String getLocalizedMessage(AppLocalizations l10n) {
    switch (statusCode) {
      case 400:
        return l10n.badRequestError;
      case 401:
        return l10n.unauthorizedError;
      case 403:
        return l10n.forbiddenError;
      case 404:
        return l10n.notFoundError;
      case 500:
        return l10n.serverError;
      default:
        return l10n.networkError;
    }
  }
}

class ValidationException extends AppException {
  final String field;
  final String validationType;

  ValidationException({required this.field, required this.validationType});

  @override
  String getLocalizedMessage(AppLocalizations l10n) {
    return l10n.validationError(field, validationType);
  }
}

class TimeoutException extends AppException {
  @override
  String getLocalizedMessage(AppLocalizations l10n) {
    return l10n.timeoutError;
  }
}

// Generic FutureBuilder wrapper with localization
class LocalizedAsyncBuilder<T> extends StatelessWidget {
  final Future<T> future;
  final Widget Function(BuildContext context, T data) builder;
  final VoidCallback? onRetry;
  final String? loadingMessage;
  final String? emptyMessage;
  final Widget? loadingWidget;
  final Widget? emptyWidget;

  const LocalizedAsyncBuilder({
    super.key,
    required this.future,
    required this.builder,
    this.onRetry,
    this.loadingMessage,
    this.emptyMessage,
    this.loadingWidget,
    this.emptyWidget,
  });

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

    return FutureBuilder<T>(
      future: future,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return loadingWidget ?? _DefaultLoadingWidget(
            message: loadingMessage ?? l10n.loading,
          );
        }

        if (snapshot.hasError) {
          return _ErrorWidget(
            error: snapshot.error!,
            onRetry: onRetry,
          );
        }

        final data = snapshot.data;
        if (data == null || (data is List && data.isEmpty)) {
          return emptyWidget ?? _DefaultEmptyWidget(
            message: emptyMessage ?? l10n.noDataAvailable,
          );
        }

        return builder(context, data);
      },
    );
  }
}

class _DefaultLoadingWidget extends StatelessWidget {
  final String message;

  const _DefaultLoadingWidget({required this.message});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Semantics(
        label: message,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const CircularProgressIndicator(),
            const SizedBox(height: 16),
            Text(message),
          ],
        ),
      ),
    );
  }
}

class _DefaultEmptyWidget extends StatelessWidget {
  final String message;

  const _DefaultEmptyWidget({required this.message});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Semantics(
        label: message,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.inbox_outlined,
              size: 64,
              color: Theme.of(context).colorScheme.outline,
            ),
            const SizedBox(height: 16),
            Text(
              message,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ],
        ),
      ),
    );
  }
}

class _ErrorWidget extends StatelessWidget {
  final Object error;
  final VoidCallback? onRetry;

  const _ErrorWidget({
    required this.error,
    this.onRetry,
  });

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

    final message = error is AppException
        ? error.getLocalizedMessage(l10n)
        : l10n.unknownError;

    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Theme.of(context).colorScheme.error,
            ),
            const SizedBox(height: 16),
            Text(
              message,
              style: Theme.of(context).textTheme.bodyLarge,
              textAlign: TextAlign.center,
            ),
            if (onRetry != null) ...[
              const SizedBox(height: 24),
              ElevatedButton.icon(
                onPressed: onRetry,
                icon: const Icon(Icons.refresh),
                label: Text(l10n.retryButton),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

Loading Skeleton with Localized Placeholder

Create shimmer loading effects with localized accessibility:

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

class LocalizedListLoader extends StatefulWidget {
  final Future<List<Product>> productsFuture;

  const LocalizedListLoader({super.key, required this.productsFuture});

  @override
  State<LocalizedListLoader> createState() => _LocalizedListLoaderState();
}

class _LocalizedListLoaderState extends State<LocalizedListLoader> {
  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return FutureBuilder<List<Product>>(
      future: widget.productsFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildSkeletonList(l10n);
        }

        if (snapshot.hasError) {
          return _buildError(l10n, snapshot.error!);
        }

        final products = snapshot.data ?? [];
        if (products.isEmpty) {
          return _buildEmpty(l10n);
        }

        return _buildProductList(l10n, products);
      },
    );
  }

  Widget _buildSkeletonList(AppLocalizations l10n) {
    return Semantics(
      label: l10n.loadingProducts,
      child: ListView.builder(
        itemCount: 5,
        itemBuilder: (context, index) => const _SkeletonProductCard(),
      ),
    );
  }

  Widget _buildError(AppLocalizations l10n, Object error) {
    return Center(
      child: Text(l10n.errorLoadingProducts),
    );
  }

  Widget _buildEmpty(AppLocalizations l10n) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.shopping_bag_outlined,
            size: 64,
            color: Theme.of(context).colorScheme.outline,
          ),
          const SizedBox(height: 16),
          Text(l10n.noProductsAvailable),
        ],
      ),
    );
  }

  Widget _buildProductList(AppLocalizations l10n, List<Product> products) {
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        final product = products[index];
        return ProductCard(
          product: product,
          priceLabel: l10n.priceLabel,
          addToCartLabel: l10n.addToCart,
        );
      },
    );
  }
}

class _SkeletonProductCard extends StatefulWidget {
  const _SkeletonProductCard();

  @override
  State<_SkeletonProductCard> createState() => _SkeletonProductCardState();
}

class _SkeletonProductCardState extends State<_SkeletonProductCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1500),
      vsync: this,
    )..repeat();
    _animation = Tween<double>(begin: -2, end: 2).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            return Row(
              children: [
                _buildShimmerBox(80, 80),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _buildShimmerBox(double.infinity, 20),
                      const SizedBox(height: 8),
                      _buildShimmerBox(100, 16),
                    ],
                  ),
                ),
              ],
            );
          },
        ),
      ),
    );
  }

  Widget _buildShimmerBox(double width, double height) {
    return Container(
      width: width,
      height: height,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(4),
        gradient: LinearGradient(
          begin: Alignment(_animation.value - 1, 0),
          end: Alignment(_animation.value + 1, 0),
          colors: [
            Colors.grey.shade300,
            Colors.grey.shade100,
            Colors.grey.shade300,
          ],
        ),
      ),
    );
  }
}

class Product {
  final String id;
  final String name;
  final double price;
  final String imageUrl;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });
}

class ProductCard extends StatelessWidget {
  final Product product;
  final String priceLabel;
  final String addToCartLabel;

  const ProductCard({
    super.key,
    required this.product,
    required this.priceLabel,
    required this.addToCartLabel,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: ListTile(
        leading: Image.network(product.imageUrl, width: 60, height: 60),
        title: Text(product.name),
        subtitle: Text('$priceLabel \$${product.price.toStringAsFixed(2)}'),
        trailing: ElevatedButton(
          onPressed: () {},
          child: Text(addToCartLabel),
        ),
      ),
    );
  }
}

Paginated FutureBuilder with Load More

Handle pagination with localized states:

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

class LocalizedPaginatedList extends StatefulWidget {
  const LocalizedPaginatedList({super.key});

  @override
  State<LocalizedPaginatedList> createState() => _LocalizedPaginatedListState();
}

class _LocalizedPaginatedListState extends State<LocalizedPaginatedList> {
  final List<Article> _articles = [];
  Future<List<Article>>? _loadMoreFuture;
  int _currentPage = 0;
  bool _hasMore = true;
  static const int _pageSize = 20;

  @override
  void initState() {
    super.initState();
    _loadInitialData();
  }

  void _loadInitialData() {
    setState(() {
      _loadMoreFuture = _fetchArticles(0);
    });
  }

  Future<List<Article>> _fetchArticles(int page) async {
    await Future.delayed(const Duration(seconds: 1));

    // Simulate API response
    final articles = List.generate(
      _pageSize,
      (index) => Article(
        id: '${page * _pageSize + index}',
        title: 'Article ${page * _pageSize + index + 1}',
        summary: 'This is the summary for article ${page * _pageSize + index + 1}',
      ),
    );

    // Simulate end of data
    if (page >= 4) {
      _hasMore = false;
    }

    return articles;
  }

  void _loadMore() {
    if (_loadMoreFuture != null || !_hasMore) return;

    setState(() {
      _currentPage++;
      _loadMoreFuture = _fetchArticles(_currentPage);
    });
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.articlesTitle)),
      body: FutureBuilder<List<Article>>(
        future: _loadMoreFuture,
        builder: (context, snapshot) {
          // Handle initial loading
          if (_articles.isEmpty && snapshot.connectionState == ConnectionState.waiting) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const CircularProgressIndicator(),
                  const SizedBox(height: 16),
                  Text(l10n.loadingArticles),
                ],
              ),
            );
          }

          // Handle initial error
          if (_articles.isEmpty && snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(l10n.errorLoadingArticles),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: _loadInitialData,
                    child: Text(l10n.retryButton),
                  ),
                ],
              ),
            );
          }

          // Add new articles when loaded
          if (snapshot.hasData && snapshot.data!.isNotEmpty) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              if (mounted) {
                setState(() {
                  _articles.addAll(snapshot.data!);
                  _loadMoreFuture = null;
                });
              }
            });
          }

          // Handle empty initial load
          if (_articles.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.article_outlined,
                    size: 64,
                    color: Theme.of(context).colorScheme.outline,
                  ),
                  const SizedBox(height: 16),
                  Text(l10n.noArticlesAvailable),
                ],
              ),
            );
          }

          return NotificationListener<ScrollNotification>(
            onNotification: (notification) {
              if (notification is ScrollEndNotification) {
                if (notification.metrics.extentAfter < 200) {
                  _loadMore();
                }
              }
              return false;
            },
            child: ListView.builder(
              itemCount: _articles.length + (_hasMore ? 1 : 0),
              itemBuilder: (context, index) {
                if (index == _articles.length) {
                  return _buildLoadMoreIndicator(l10n, snapshot);
                }
                return _buildArticleCard(l10n, _articles[index]);
              },
            ),
          );
        },
      ),
    );
  }

  Widget _buildArticleCard(AppLocalizations l10n, Article article) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: ListTile(
        title: Text(article.title),
        subtitle: Text(article.summary),
        trailing: Text(l10n.readMore),
        onTap: () {},
      ),
    );
  }

  Widget _buildLoadMoreIndicator(
    AppLocalizations l10n,
    AsyncSnapshot<List<Article>> snapshot,
  ) {
    if (snapshot.hasError) {
      return Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text(
              l10n.errorLoadingMore,
              style: TextStyle(color: Theme.of(context).colorScheme.error),
            ),
            const SizedBox(height: 8),
            TextButton(
              onPressed: _loadMore,
              child: Text(l10n.retryButton),
            ),
          ],
        ),
      );
    }

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Center(
        child: Column(
          children: [
            const CircularProgressIndicator(),
            const SizedBox(height: 8),
            Text(l10n.loadingMore),
          ],
        ),
      ),
    );
  }
}

class Article {
  final String id;
  final String title;
  final String summary;

  Article({
    required this.id,
    required this.title,
    required this.summary,
  });
}

Caching with Localized Freshness Indicators

Show data freshness with localized timestamps:

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

class CachedDataBuilder<T> extends StatefulWidget {
  final Future<T> Function() fetchData;
  final Widget Function(BuildContext context, T data) builder;
  final Duration cacheDuration;
  final String? cacheKey;

  const CachedDataBuilder({
    super.key,
    required this.fetchData,
    required this.builder,
    this.cacheDuration = const Duration(minutes: 5),
    this.cacheKey,
  });

  @override
  State<CachedDataBuilder<T>> createState() => _CachedDataBuilderState<T>();
}

class _CachedDataBuilderState<T> extends State<CachedDataBuilder<T>> {
  T? _cachedData;
  DateTime? _lastFetched;
  Future<T>? _fetchFuture;
  bool _isRefreshing = false;

  @override
  void initState() {
    super.initState();
    _fetchFuture = _loadData();
  }

  Future<T> _loadData() async {
    final data = await widget.fetchData();
    setState(() {
      _cachedData = data;
      _lastFetched = DateTime.now();
    });
    return data;
  }

  Future<void> _refresh() async {
    if (_isRefreshing) return;

    setState(() {
      _isRefreshing = true;
    });

    try {
      await _loadData();
    } finally {
      if (mounted) {
        setState(() {
          _isRefreshing = false;
        });
      }
    }
  }

  bool get _isStale {
    if (_lastFetched == null) return true;
    return DateTime.now().difference(_lastFetched!) > widget.cacheDuration;
  }

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

    return FutureBuilder<T>(
      future: _fetchFuture,
      builder: (context, snapshot) {
        // Show cached data while refreshing
        if (_cachedData != null) {
          return Column(
            children: [
              _buildFreshnessIndicator(l10n),
              Expanded(
                child: RefreshIndicator(
                  onRefresh: _refresh,
                  child: widget.builder(context, _cachedData as T),
                ),
              ),
            ],
          );
        }

        // Initial loading
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const CircularProgressIndicator(),
                const SizedBox(height: 16),
                Text(l10n.loading),
              ],
            ),
          );
        }

        // Error with no cache
        if (snapshot.hasError) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  Icons.error_outline,
                  size: 64,
                  color: Theme.of(context).colorScheme.error,
                ),
                const SizedBox(height: 16),
                Text(l10n.errorLoading),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _fetchFuture = _loadData();
                    });
                  },
                  child: Text(l10n.retryButton),
                ),
              ],
            ),
          );
        }

        // Should not reach here, but handle gracefully
        return Center(child: Text(l10n.noDataAvailable));
      },
    );
  }

  Widget _buildFreshnessIndicator(AppLocalizations l10n) {
    final theme = Theme.of(context);
    final freshnessText = _getFreshnessText(l10n);

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: _isStale
          ? theme.colorScheme.errorContainer.withOpacity(0.3)
          : theme.colorScheme.primaryContainer.withOpacity(0.3),
      child: Row(
        children: [
          Icon(
            _isStale ? Icons.warning_amber : Icons.check_circle,
            size: 16,
            color: _isStale
                ? theme.colorScheme.error
                : theme.colorScheme.primary,
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              freshnessText,
              style: theme.textTheme.bodySmall,
            ),
          ),
          if (_isRefreshing)
            const SizedBox(
              width: 16,
              height: 16,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          else
            TextButton(
              onPressed: _refresh,
              child: Text(l10n.refreshButton),
            ),
        ],
      ),
    );
  }

  String _getFreshnessText(AppLocalizations l10n) {
    if (_lastFetched == null) {
      return l10n.noDataCached;
    }

    final now = DateTime.now();
    final difference = now.difference(_lastFetched!);

    if (difference.inMinutes < 1) {
      return l10n.updatedJustNow;
    } else if (difference.inMinutes < 60) {
      return l10n.updatedMinutesAgo(difference.inMinutes);
    } else if (difference.inHours < 24) {
      return l10n.updatedHoursAgo(difference.inHours);
    } else {
      final formatter = DateFormat.yMMMd(
        Localizations.localeOf(context).languageCode,
      );
      return l10n.updatedOn(formatter.format(_lastFetched!));
    }
  }
}

Accessibility for Async States

Ensure screen readers announce state changes:

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

class AccessibleFutureBuilder<T> extends StatefulWidget {
  final Future<T> future;
  final Widget Function(T data) builder;
  final String? loadingAnnouncement;
  final String? successAnnouncement;
  final String? errorAnnouncement;

  const AccessibleFutureBuilder({
    super.key,
    required this.future,
    required this.builder,
    this.loadingAnnouncement,
    this.successAnnouncement,
    this.errorAnnouncement,
  });

  @override
  State<AccessibleFutureBuilder<T>> createState() =>
      _AccessibleFutureBuilderState<T>();
}

class _AccessibleFutureBuilderState<T>
    extends State<AccessibleFutureBuilder<T>> {
  ConnectionState? _previousState;

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

    return FutureBuilder<T>(
      future: widget.future,
      builder: (context, snapshot) {
        // Announce state changes
        _announceStateChange(context, l10n, snapshot);
        _previousState = snapshot.connectionState;

        if (snapshot.connectionState == ConnectionState.waiting) {
          return Semantics(
            liveRegion: true,
            label: widget.loadingAnnouncement ?? l10n.loading,
            child: const Center(
              child: CircularProgressIndicator(),
            ),
          );
        }

        if (snapshot.hasError) {
          return Semantics(
            liveRegion: true,
            label: widget.errorAnnouncement ?? l10n.errorOccurred,
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.error,
                    size: 48,
                    color: Theme.of(context).colorScheme.error,
                    semanticLabel: l10n.errorIcon,
                  ),
                  const SizedBox(height: 16),
                  Text(l10n.errorOccurred),
                ],
              ),
            ),
          );
        }

        if (!snapshot.hasData) {
          return Semantics(
            liveRegion: true,
            label: l10n.noDataAvailable,
            child: Center(
              child: Text(l10n.noDataAvailable),
            ),
          );
        }

        return Semantics(
          liveRegion: true,
          label: widget.successAnnouncement ?? l10n.contentLoaded,
          child: widget.builder(snapshot.data as T),
        );
      },
    );
  }

  void _announceStateChange(
    BuildContext context,
    AppLocalizations l10n,
    AsyncSnapshot<T> snapshot,
  ) {
    if (_previousState == snapshot.connectionState) return;

    String? announcement;

    if (snapshot.connectionState == ConnectionState.waiting) {
      announcement = widget.loadingAnnouncement ?? l10n.loading;
    } else if (snapshot.hasError) {
      announcement = widget.errorAnnouncement ?? l10n.errorOccurred;
    } else if (snapshot.hasData) {
      announcement = widget.successAnnouncement ?? l10n.contentLoaded;
    }

    if (announcement != null) {
      SemanticsService.announce(announcement, TextDirection.ltr);
    }
  }
}

Complete ARB File for FutureBuilder

{
  "@@locale": "en",

  "loading": "Loading...",
  "@loading": {
    "description": "Generic loading message"
  },
  "loadingProfile": "Loading your profile...",
  "@loadingProfile": {
    "description": "Loading message for profile"
  },
  "loadingProducts": "Loading products...",
  "@loadingProducts": {
    "description": "Loading message for products"
  },
  "loadingArticles": "Loading articles...",
  "@loadingArticles": {
    "description": "Loading message for articles"
  },
  "loadingMore": "Loading more...",
  "@loadingMore": {
    "description": "Message when loading additional content"
  },

  "errorOccurred": "An error occurred",
  "@errorOccurred": {
    "description": "Generic error message"
  },
  "errorLoading": "Failed to load data",
  "@errorLoading": {
    "description": "Generic loading error"
  },
  "errorLoadingProfile": "Failed to load profile",
  "@errorLoadingProfile": {
    "description": "Profile loading error"
  },
  "errorLoadingProducts": "Failed to load products",
  "@errorLoadingProducts": {
    "description": "Products loading error"
  },
  "errorLoadingArticles": "Failed to load articles",
  "@errorLoadingArticles": {
    "description": "Articles loading error"
  },
  "errorLoadingMore": "Failed to load more items",
  "@errorLoadingMore": {
    "description": "Pagination error"
  },

  "networkError": "Please check your internet connection",
  "@networkError": {
    "description": "Network connectivity error"
  },
  "timeoutError": "Request timed out. Please try again.",
  "@timeoutError": {
    "description": "Request timeout error"
  },
  "serverError": "Server error. Please try again later.",
  "@serverError": {
    "description": "Server-side error"
  },
  "badRequestError": "Invalid request. Please check your input.",
  "@badRequestError": {
    "description": "Bad request error"
  },
  "unauthorizedError": "Please log in to continue",
  "@unauthorizedError": {
    "description": "Authentication required error"
  },
  "forbiddenError": "You don't have permission to access this",
  "@forbiddenError": {
    "description": "Access denied error"
  },
  "notFoundError": "The requested item was not found",
  "@notFoundError": {
    "description": "Resource not found error"
  },
  "unknownError": "Something went wrong. Please try again.",
  "@unknownError": {
    "description": "Unknown error fallback"
  },
  "validationError": "Invalid {field}: {validationType}",
  "@validationError": {
    "description": "Field validation error",
    "placeholders": {
      "field": {"type": "String"},
      "validationType": {"type": "String"}
    }
  },

  "noDataAvailable": "No data available",
  "@noDataAvailable": {
    "description": "Empty state message"
  },
  "noProductsAvailable": "No products found",
  "@noProductsAvailable": {
    "description": "Empty products message"
  },
  "noArticlesAvailable": "No articles yet",
  "@noArticlesAvailable": {
    "description": "Empty articles message"
  },
  "profileNotFound": "Profile not found",
  "@profileNotFound": {
    "description": "Missing profile message"
  },

  "retryButton": "Try Again",
  "@retryButton": {
    "description": "Retry action button"
  },
  "refreshButton": "Refresh",
  "@refreshButton": {
    "description": "Refresh action button"
  },
  "readMore": "Read more",
  "@readMore": {
    "description": "Read more link text"
  },
  "addToCart": "Add to Cart",
  "@addToCart": {
    "description": "Add to cart button"
  },

  "contentLoaded": "Content loaded successfully",
  "@contentLoaded": {
    "description": "Success announcement for screen readers"
  },
  "errorIcon": "Error icon",
  "@errorIcon": {
    "description": "Accessibility label for error icon"
  },

  "noDataCached": "No cached data",
  "@noDataCached": {
    "description": "No cache available message"
  },
  "updatedJustNow": "Updated just now",
  "@updatedJustNow": {
    "description": "Data freshness - just updated"
  },
  "updatedMinutesAgo": "Updated {minutes} {minutes, plural, =1{minute} other{minutes}} ago",
  "@updatedMinutesAgo": {
    "description": "Data freshness in minutes",
    "placeholders": {
      "minutes": {"type": "int"}
    }
  },
  "updatedHoursAgo": "Updated {hours} {hours, plural, =1{hour} other{hours}} ago",
  "@updatedHoursAgo": {
    "description": "Data freshness in hours",
    "placeholders": {
      "hours": {"type": "int"}
    }
  },
  "updatedOn": "Updated on {date}",
  "@updatedOn": {
    "description": "Data freshness with date",
    "placeholders": {
      "date": {"type": "String"}
    }
  },

  "profileTitle": "Profile",
  "@profileTitle": {
    "description": "Profile section title"
  },
  "articlesTitle": "Articles",
  "@articlesTitle": {
    "description": "Articles page title"
  },
  "nameLabel": "Name",
  "@nameLabel": {
    "description": "Name field label"
  },
  "emailLabel": "Email",
  "@emailLabel": {
    "description": "Email field label"
  },
  "priceLabel": "Price:",
  "@priceLabel": {
    "description": "Price field label"
  }
}

Best Practices Summary

  1. Always handle all states: Loading, error, empty, and success
  2. Use typed exceptions: Create custom exception classes with localized messages
  3. Provide retry mechanisms: Let users recover from errors
  4. Show loading skeletons: Better UX than spinners for list content
  5. Announce state changes: Use SemanticsService.announce for accessibility
  6. Cache when appropriate: Show stale data while refreshing
  7. Localize time-relative strings: "Updated 5 minutes ago" needs proper pluralization
  8. Use consistent error patterns: Create reusable error widgets
  9. Handle pagination states: Separate loading states for initial load vs load more
  10. Test with slow networks: Simulate delays to ensure good loading UX

Conclusion

Proper FutureBuilder localization ensures your async operations provide excellent user experiences across all languages. By handling loading states, error messages, empty states, and success displays with localized content, you create apps that feel native to users worldwide. Remember to always test your async flows with various network conditions and verify that screen readers properly announce state changes.

The reusable patterns shown here—typed error handling, cached data builders, and accessible future builders—can be adapted to any Flutter application requiring robust async localization support.