← Back to Blog

Flutter Hero Animation Localization: Shared Element Transitions with Multilingual Content

flutterheroanimationtransitionslocalizationaccessibility

Flutter Hero Animation Localization: Shared Element Transitions with Multilingual Content

Hero animations create beautiful shared element transitions between screens. Localizing Hero widgets requires handling text that animates between different states, ensuring smooth transitions with varying text lengths, and maintaining accessibility. This guide covers everything you need to know about localizing Hero animations in Flutter.

Understanding Hero Localization

Hero widgets require localization for:

  • Transitioning titles: Text that animates between screens
  • Subtitle and description text: Secondary content in transitions
  • Tag labels: Identifiers shown during animations
  • Action buttons: Interactive elements with tooltips
  • RTL support: Proper direction handling during transitions
  • Accessibility announcements: Screen reader feedback

Basic Hero with Localized Title

Start with a simple localized hero transition:

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

class LocalizedHeroList extends StatelessWidget {
  const LocalizedHeroList({super.key});

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.productsTitle),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 10,
        itemBuilder: (context, index) {
          final productId = index + 1;

          return Card(
            margin: const EdgeInsets.only(bottom: 12),
            child: ListTile(
              leading: Hero(
                tag: 'product-image-$productId',
                child: Container(
                  width: 56,
                  height: 56,
                  decoration: BoxDecoration(
                    color: Colors.primaries[index % Colors.primaries.length],
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: const Icon(Icons.shopping_bag, color: Colors.white),
                ),
              ),
              title: Hero(
                tag: 'product-title-$productId',
                child: Material(
                  color: Colors.transparent,
                  child: Text(
                    l10n.productName(productId),
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                ),
              ),
              subtitle: Text(l10n.productPrice(19.99 + index * 5)),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ProductDetailScreen(
                      productId: productId,
                      productIndex: index,
                    ),
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

class ProductDetailScreen extends StatelessWidget {
  final int productId;
  final int productIndex;

  const ProductDetailScreen({
    super.key,
    required this.productId,
    required this.productIndex,
  });

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

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 300,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Hero(
                tag: 'product-title-$productId',
                child: Material(
                  color: Colors.transparent,
                  child: Text(
                    l10n.productName(productId),
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              background: Hero(
                tag: 'product-image-$productId',
                child: Container(
                  color: Colors.primaries[productIndex % Colors.primaries.length],
                  child: const Icon(
                    Icons.shopping_bag,
                    size: 100,
                    color: Colors.white54,
                  ),
                ),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    l10n.productPrice(19.99 + productIndex * 5),
                    style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                          color: Theme.of(context).primaryColor,
                          fontWeight: FontWeight.bold,
                        ),
                  ),
                  const SizedBox(height: 16),
                  Text(
                    l10n.productDescription,
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                  const SizedBox(height: 24),
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton(
                      onPressed: () {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text(l10n.addedToCartMessage),
                          ),
                        );
                      },
                      child: Text(l10n.addToCartButton),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Hero with Flight Shuttle Builder

Customize the animation for different text lengths:

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

class FlightShuttleHero extends StatelessWidget {
  const FlightShuttleHero({super.key});

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.articlesTitle),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 5,
        itemBuilder: (context, index) {
          final articleId = index + 1;

          return Card(
            margin: const EdgeInsets.only(bottom: 16),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ArticleDetailScreen(
                      articleId: articleId,
                    ),
                  ),
                );
              },
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Hero(
                      tag: 'article-title-$articleId',
                      flightShuttleBuilder: (
                        flightContext,
                        animation,
                        flightDirection,
                        fromHeroContext,
                        toHeroContext,
                      ) {
                        return AnimatedBuilder(
                          animation: animation,
                          builder: (context, child) {
                            return Material(
                              color: Colors.transparent,
                              child: Text(
                                l10n.articleTitle(articleId),
                                style: TextStyle.lerp(
                                  Theme.of(fromHeroContext).textTheme.titleMedium,
                                  Theme.of(toHeroContext).textTheme.headlineSmall,
                                  animation.value,
                                ),
                                maxLines: animation.value < 0.5 ? 2 : 3,
                                overflow: TextOverflow.ellipsis,
                              ),
                            );
                          },
                        );
                      },
                      child: Text(
                        l10n.articleTitle(articleId),
                        style: Theme.of(context).textTheme.titleMedium,
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      l10n.articleExcerpt,
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                            color: Colors.grey[600],
                          ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 8),
                    Text(
                      l10n.articleDate,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                            color: Colors.grey[500],
                          ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class ArticleDetailScreen extends StatelessWidget {
  final int articleId;

  const ArticleDetailScreen({
    super.key,
    required this.articleId,
  });

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.articleLabel),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Hero(
              tag: 'article-title-$articleId',
              child: Material(
                color: Colors.transparent,
                child: Text(
                  l10n.articleTitle(articleId),
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
              ),
            ),
            const SizedBox(height: 8),
            Text(
              l10n.articleDate,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                    color: Colors.grey[500],
                  ),
            ),
            const SizedBox(height: 16),
            Text(
              l10n.articleFullContent,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ],
        ),
      ),
    );
  }
}

Hero Gallery with Localized Captions

Create a photo gallery with transitioning captions:

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

class HeroGallery extends StatelessWidget {
  const HeroGallery({super.key});

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.galleryTitle),
      ),
      body: GridView.builder(
        padding: const EdgeInsets.all(8),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
          childAspectRatio: 0.8,
        ),
        itemCount: 8,
        itemBuilder: (context, index) {
          final photoId = index + 1;

          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => PhotoDetailScreen(
                    photoId: photoId,
                    colorIndex: index,
                  ),
                ),
              );
            },
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Expanded(
                  child: Hero(
                    tag: 'photo-$photoId',
                    child: Container(
                      width: double.infinity,
                      decoration: BoxDecoration(
                        color: Colors.primaries[index % Colors.primaries.length],
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: Icon(
                        Icons.photo,
                        size: 48,
                        color: Colors.white.withOpacity(0.5),
                      ),
                    ),
                  ),
                ),
                const SizedBox(height: 8),
                Hero(
                  tag: 'photo-title-$photoId',
                  child: Material(
                    color: Colors.transparent,
                    child: Text(
                      l10n.photoTitle(photoId),
                      style: Theme.of(context).textTheme.titleSmall,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ),
                Hero(
                  tag: 'photo-location-$photoId',
                  child: Material(
                    color: Colors.transparent,
                    child: Text(
                      l10n.photoLocation(photoId),
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                            color: Colors.grey[600],
                          ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

class PhotoDetailScreen extends StatelessWidget {
  final int photoId;
  final int colorIndex;

  const PhotoDetailScreen({
    super.key,
    required this.photoId,
    required this.colorIndex,
  });

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

    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        iconTheme: const IconThemeData(color: Colors.white),
        actions: [
          IconButton(
            icon: const Icon(Icons.share),
            tooltip: l10n.shareTooltip,
            onPressed: () {},
          ),
          IconButton(
            icon: const Icon(Icons.favorite_border),
            tooltip: l10n.favoriteTooltip,
            onPressed: () {},
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: Center(
              child: Hero(
                tag: 'photo-$photoId',
                child: Container(
                  margin: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.primaries[colorIndex % Colors.primaries.length],
                    borderRadius: BorderRadius.circular(16),
                  ),
                  child: Icon(
                    Icons.photo,
                    size: 120,
                    color: Colors.white.withOpacity(0.5),
                  ),
                ),
              ),
            ),
          ),
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(24),
            decoration: BoxDecoration(
              color: Colors.grey[900],
              borderRadius: const BorderRadius.vertical(
                top: Radius.circular(24),
              ),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Hero(
                  tag: 'photo-title-$photoId',
                  child: Material(
                    color: Colors.transparent,
                    child: Text(
                      l10n.photoTitle(photoId),
                      style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                            color: Colors.white,
                          ),
                    ),
                  ),
                ),
                const SizedBox(height: 8),
                Hero(
                  tag: 'photo-location-$photoId',
                  child: Material(
                    color: Colors.transparent,
                    child: Row(
                      children: [
                        Icon(Icons.location_on, size: 16, color: Colors.grey[400]),
                        const SizedBox(width: 4),
                        Text(
                          l10n.photoLocation(photoId),
                          style: TextStyle(color: Colors.grey[400]),
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 16),
                Text(
                  l10n.photoDescription,
                  style: TextStyle(color: Colors.grey[300]),
                ),
                const SizedBox(height: 16),
                Row(
                  children: [
                    Icon(Icons.calendar_today, size: 14, color: Colors.grey[500]),
                    const SizedBox(width: 4),
                    Text(
                      l10n.photoDate,
                      style: TextStyle(
                        color: Colors.grey[500],
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Hero with RTL Support

Handle right-to-left layouts properly:

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

class RTLHeroCards extends StatelessWidget {
  const RTLHeroCards({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.teamTitle),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 5,
        itemBuilder: (context, index) {
          final memberId = index + 1;

          return Card(
            margin: const EdgeInsets.only(bottom: 12),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => TeamMemberDetail(
                      memberId: memberId,
                    ),
                  ),
                );
              },
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Row(
                  children: [
                    Hero(
                      tag: 'avatar-$memberId',
                      child: CircleAvatar(
                        radius: 30,
                        backgroundColor: Colors.primaries[index % Colors.primaries.length],
                        child: Text(
                          l10n.memberInitial(memberId),
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Hero(
                            tag: 'name-$memberId',
                            child: Material(
                              color: Colors.transparent,
                              child: Text(
                                l10n.memberName(memberId),
                                style: Theme.of(context).textTheme.titleMedium,
                              ),
                            ),
                          ),
                          Hero(
                            tag: 'role-$memberId',
                            child: Material(
                              color: Colors.transparent,
                              child: Text(
                                l10n.memberRole(memberId),
                                style: Theme.of(context).textTheme.bodySmall?.copyWith(
                                      color: Colors.grey[600],
                                    ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                    Icon(
                      isRTL ? Icons.chevron_left : Icons.chevron_right,
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class TeamMemberDetail extends StatelessWidget {
  final int memberId;

  const TeamMemberDetail({
    super.key,
    required this.memberId,
  });

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.profileLabel),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            const SizedBox(height: 32),
            Hero(
              tag: 'avatar-$memberId',
              child: CircleAvatar(
                radius: 60,
                backgroundColor: Colors.primaries[(memberId - 1) % Colors.primaries.length],
                child: Text(
                  l10n.memberInitial(memberId),
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 40,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
            const SizedBox(height: 16),
            Hero(
              tag: 'name-$memberId',
              child: Material(
                color: Colors.transparent,
                child: Text(
                  l10n.memberName(memberId),
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
              ),
            ),
            const SizedBox(height: 4),
            Hero(
              tag: 'role-$memberId',
              child: Material(
                color: Colors.transparent,
                child: Text(
                  l10n.memberRole(memberId),
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                        color: Colors.grey[600],
                      ),
                ),
              ),
            ),
            const SizedBox(height: 24),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 32),
              child: Text(
                l10n.memberBio,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ),
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                _buildContactButton(
                  context,
                  Icons.email,
                  l10n.emailTooltip,
                ),
                const SizedBox(width: 16),
                _buildContactButton(
                  context,
                  Icons.phone,
                  l10n.phoneTooltip,
                ),
                const SizedBox(width: 16),
                _buildContactButton(
                  context,
                  Icons.chat,
                  l10n.messageTooltip,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildContactButton(BuildContext context, IconData icon, String tooltip) {
    return Tooltip(
      message: tooltip,
      child: CircleAvatar(
        radius: 24,
        backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
        child: IconButton(
          icon: Icon(icon, color: Theme.of(context).primaryColor),
          onPressed: () {},
        ),
      ),
    );
  }
}

Accessibility for Hero Animations

Ensure proper accessibility labels:

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

class AccessibleHero extends StatelessWidget {
  const AccessibleHero({super.key});

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.destinationsTitle),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 6,
        itemBuilder: (context, index) {
          final destinationId = index + 1;

          return Semantics(
            button: true,
            label: l10n.destinationAccessibilityLabel(
              l10n.destinationName(destinationId),
              l10n.destinationCountry(destinationId),
            ),
            child: Card(
              margin: const EdgeInsets.only(bottom: 16),
              clipBehavior: Clip.antiAlias,
              child: InkWell(
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => DestinationDetail(
                        destinationId: destinationId,
                        colorIndex: index,
                      ),
                    ),
                  );
                },
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Hero(
                      tag: 'destination-image-$destinationId',
                      child: Container(
                        height: 150,
                        width: double.infinity,
                        color: Colors.primaries[index % Colors.primaries.length],
                        child: const Icon(
                          Icons.landscape,
                          size: 64,
                          color: Colors.white54,
                        ),
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Hero(
                            tag: 'destination-name-$destinationId',
                            child: Material(
                              color: Colors.transparent,
                              child: Text(
                                l10n.destinationName(destinationId),
                                style: Theme.of(context).textTheme.titleLarge,
                              ),
                            ),
                          ),
                          const SizedBox(height: 4),
                          Hero(
                            tag: 'destination-country-$destinationId',
                            child: Material(
                              color: Colors.transparent,
                              child: Row(
                                children: [
                                  Icon(
                                    Icons.location_on,
                                    size: 14,
                                    color: Colors.grey[600],
                                  ),
                                  const SizedBox(width: 4),
                                  Text(
                                    l10n.destinationCountry(destinationId),
                                    style: TextStyle(color: Colors.grey[600]),
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class DestinationDetail extends StatelessWidget {
  final int destinationId;
  final int colorIndex;

  const DestinationDetail({
    super.key,
    required this.destinationId,
    required this.colorIndex,
  });

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

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 250,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Hero(
                tag: 'destination-name-$destinationId',
                child: Material(
                  color: Colors.transparent,
                  child: Text(
                    l10n.destinationName(destinationId),
                    style: const TextStyle(
                      color: Colors.white,
                      shadows: [
                        Shadow(
                          offset: Offset(1, 1),
                          blurRadius: 4,
                          color: Colors.black45,
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              background: Hero(
                tag: 'destination-image-$destinationId',
                child: Container(
                  color: Colors.primaries[colorIndex % Colors.primaries.length],
                  child: const Icon(
                    Icons.landscape,
                    size: 120,
                    color: Colors.white24,
                  ),
                ),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Hero(
                    tag: 'destination-country-$destinationId',
                    child: Material(
                      color: Colors.transparent,
                      child: Row(
                        children: [
                          Icon(
                            Icons.location_on,
                            size: 18,
                            color: Theme.of(context).primaryColor,
                          ),
                          const SizedBox(width: 4),
                          Text(
                            l10n.destinationCountry(destinationId),
                            style: TextStyle(
                              color: Theme.of(context).primaryColor,
                              fontSize: 16,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  Semantics(
                    header: true,
                    child: Text(
                      l10n.aboutDestinationTitle,
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    l10n.destinationDescription,
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                  const SizedBox(height: 24),
                  Semantics(
                    button: true,
                    label: l10n.bookNowAccessibilityLabel,
                    child: SizedBox(
                      width: double.infinity,
                      child: ElevatedButton(
                        onPressed: () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text(l10n.bookingStartedMessage),
                            ),
                          );
                        },
                        child: Text(l10n.bookNowButton),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Hero with Placeholder During Transition

Handle long text with placeholders:

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

class PlaceholderHero extends StatelessWidget {
  const PlaceholderHero({super.key});

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.eventsTitle),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 5,
        itemBuilder: (context, index) {
          final eventId = index + 1;

          return Card(
            margin: const EdgeInsets.only(bottom: 16),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => EventDetailScreen(
                      eventId: eventId,
                    ),
                  ),
                );
              },
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    Hero(
                      tag: 'event-date-$eventId',
                      placeholderBuilder: (context, size, child) {
                        return Container(
                          width: size.width,
                          height: size.height,
                          decoration: BoxDecoration(
                            color: Theme.of(context).primaryColor.withOpacity(0.1),
                            borderRadius: BorderRadius.circular(8),
                          ),
                        );
                      },
                      child: Container(
                        width: 60,
                        height: 60,
                        decoration: BoxDecoration(
                          color: Theme.of(context).primaryColor,
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              l10n.eventDay(eventId),
                              style: const TextStyle(
                                color: Colors.white,
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            Text(
                              l10n.eventMonth,
                              style: const TextStyle(
                                color: Colors.white70,
                                fontSize: 12,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Hero(
                            tag: 'event-title-$eventId',
                            child: Material(
                              color: Colors.transparent,
                              child: Text(
                                l10n.eventTitle(eventId),
                                style: Theme.of(context).textTheme.titleMedium,
                                maxLines: 2,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ),
                          ),
                          const SizedBox(height: 4),
                          Text(
                            l10n.eventTime,
                            style: TextStyle(color: Colors.grey[600]),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class EventDetailScreen extends StatelessWidget {
  final int eventId;

  const EventDetailScreen({
    super.key,
    required this.eventId,
  });

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.eventDetailsLabel),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Hero(
                  tag: 'event-date-$eventId',
                  child: Container(
                    width: 80,
                    height: 80,
                    decoration: BoxDecoration(
                      color: Theme.of(context).primaryColor,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          l10n.eventDay(eventId),
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 28,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Text(
                          l10n.eventMonth,
                          style: const TextStyle(
                            color: Colors.white70,
                            fontSize: 14,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Hero(
                        tag: 'event-title-$eventId',
                        child: Material(
                          color: Colors.transparent,
                          child: Text(
                            l10n.eventTitle(eventId),
                            style: Theme.of(context).textTheme.headlineSmall,
                          ),
                        ),
                      ),
                      const SizedBox(height: 8),
                      Row(
                        children: [
                          Icon(Icons.access_time, size: 16, color: Colors.grey[600]),
                          const SizedBox(width: 4),
                          Text(
                            l10n.eventTime,
                            style: TextStyle(color: Colors.grey[600]),
                          ),
                        ],
                      ),
                      const SizedBox(height: 4),
                      Row(
                        children: [
                          Icon(Icons.location_on, size: 16, color: Colors.grey[600]),
                          const SizedBox(width: 4),
                          Expanded(
                            child: Text(
                              l10n.eventLocation,
                              style: TextStyle(color: Colors.grey[600]),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
            const SizedBox(height: 24),
            Text(
              l10n.eventDescriptionTitle,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Text(
              l10n.eventDescription,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text(l10n.registeredMessage),
                    ),
                  );
                },
                child: Text(l10n.registerButton),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

ARB Translations for Hero

Add these entries to your ARB files:

{
  "productsTitle": "Products",
  "@productsTitle": {
    "description": "Title for products screen"
  },
  "productName": "Product {number}",
  "@productName": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "productPrice": "${price}",
  "@productPrice": {
    "placeholders": {
      "price": {"type": "double", "format": "currency", "optionalParameters": {"symbol": "$"}}
    }
  },
  "productDescription": "This is a high-quality product that meets all your needs. Made with premium materials for lasting durability.",
  "addToCartButton": "Add to Cart",
  "addedToCartMessage": "Added to cart successfully",

  "articlesTitle": "Articles",
  "articleLabel": "Article",
  "articleTitle": "Article {number}: Understanding Flutter Hero Animations",
  "@articleTitle": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "articleExcerpt": "Learn how to create beautiful shared element transitions in your Flutter applications...",
  "articleDate": "January 14, 2024",
  "articleFullContent": "Hero animations create seamless visual continuity when transitioning between screens. In this article, we explore best practices for implementing hero animations with localized content...",

  "galleryTitle": "Gallery",
  "photoTitle": "Photo {number}",
  "@photoTitle": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "photoLocation": "Location {number}",
  "@photoLocation": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "photoDescription": "A beautiful captured moment showcasing the essence of this location.",
  "photoDate": "Captured on January 14, 2024",
  "shareTooltip": "Share",
  "favoriteTooltip": "Add to favorites",

  "teamTitle": "Our Team",
  "profileLabel": "Profile",
  "memberInitial": "M{number}",
  "@memberInitial": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "memberName": "Team Member {number}",
  "@memberName": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "memberRole": "Senior Developer",
  "@memberRole": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "memberBio": "A passionate developer with years of experience building amazing applications. Specializes in mobile development and user experience design.",
  "emailTooltip": "Send email",
  "phoneTooltip": "Call",
  "messageTooltip": "Send message",

  "destinationsTitle": "Destinations",
  "destinationName": "Destination {number}",
  "@destinationName": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "destinationCountry": "Country {number}",
  "@destinationCountry": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "destinationAccessibilityLabel": "View {name} in {country}",
  "@destinationAccessibilityLabel": {
    "placeholders": {
      "name": {"type": "String"},
      "country": {"type": "String"}
    }
  },
  "aboutDestinationTitle": "About This Destination",
  "destinationDescription": "Discover the beauty and culture of this amazing destination. From stunning landscapes to rich history, there's something for everyone.",
  "bookNowButton": "Book Now",
  "bookNowAccessibilityLabel": "Book this destination now",
  "bookingStartedMessage": "Starting booking process...",

  "eventsTitle": "Events",
  "eventDetailsLabel": "Event Details",
  "eventDay": "{day}",
  "@eventDay": {
    "placeholders": {
      "day": {"type": "int"}
    }
  },
  "eventMonth": "JAN",
  "eventTitle": "Event {number}: Annual Conference",
  "@eventTitle": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },
  "eventTime": "10:00 AM - 5:00 PM",
  "eventLocation": "Convention Center, Main Hall",
  "eventDescriptionTitle": "About This Event",
  "eventDescription": "Join us for our annual conference featuring industry experts, networking opportunities, and hands-on workshops. Don't miss this opportunity to learn and connect.",
  "registerButton": "Register Now",
  "registeredMessage": "Successfully registered for event"
}

Testing Hero Localization

Write tests for your localized Hero:

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

void main() {
  group('LocalizedHero', () {
    testWidgets('displays localized product names', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('en'),
          home: const LocalizedHeroList(),
        ),
      );

      expect(find.text('Product 1'), findsOneWidget);
      expect(find.text('Products'), findsOneWidget);
    });

    testWidgets('navigates to detail with hero animation', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('en'),
          home: const LocalizedHeroList(),
        ),
      );

      await tester.tap(find.text('Product 1'));
      await tester.pumpAndSettle();

      // Verify we're on the detail screen
      expect(find.text('Add to Cart'), 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: RTLHeroCards(),
          ),
        ),
      );

      // Verify RTL chevron icon direction
      expect(find.byIcon(Icons.chevron_left), findsWidgets);
    });

    testWidgets('shows localized content in Spanish', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('es'),
          home: const LocalizedHeroList(),
        ),
      );

      expect(find.text('Productos'), findsOneWidget);
      expect(find.text('Producto 1'), findsOneWidget);
    });
  });
}

Summary

Localizing Hero animations in Flutter requires:

  1. Transitioning text that animates smoothly between screens
  2. Flight shuttle builders for custom text interpolation
  3. Placeholder widgets during transitions
  4. RTL support with proper icon and layout mirroring
  5. Accessibility labels for screen reader users
  6. Consistent hero tags across navigation
  7. Material wrappers for text to animate properly
  8. Comprehensive testing across different locales

Hero animations create delightful user experiences, and proper localization ensures your transitions look polished and feel natural for users in any language.