← Back to Blog

Flutter RotationTransition Localization: Dynamic Spinning Animations for Multilingual Apps

flutterrotationtransitionanimationrotationlocalizationrtl

Flutter RotationTransition Localization: Dynamic Spinning Animations for Multilingual Apps

RotationTransition provides explicit control over rotation animations using an Animation controller. Proper localization ensures that spinning elements, directional indicators, and loading animations work seamlessly across languages with correct RTL handling. This guide covers comprehensive strategies for localizing RotationTransition widgets in Flutter.

Understanding RotationTransition Localization

RotationTransition widgets require localization for:

  • Loading spinners: Rotation direction and accessibility announcements
  • Directional icons: Arrows and chevrons that adapt to RTL layouts
  • Refresh indicators: Pull-to-refresh animations with status updates
  • Expandable controls: Rotation icons for collapsible sections
  • Processing states: Visual feedback during operations
  • Accessibility feedback: Screen reader announcements for rotation states

Basic RotationTransition with Accessibility

Start with a loading indicator with proper accessibility:

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

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

  @override
  State<LocalizedLoadingIndicator> createState() => _LocalizedLoadingIndicatorState();
}

class _LocalizedLoadingIndicatorState extends State<LocalizedLoadingIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
  }

  void _startLoading() {
    setState(() => _isLoading = true);
    _controller.repeat();
  }

  void _stopLoading() {
    _controller.stop();
    setState(() => _isLoading = false);
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.loadingDemoTitle)),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Semantics(
              label: _isLoading
                  ? l10n.loadingInProgress
                  : l10n.loadingIdle,
              liveRegion: true,
              child: Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.primaryContainer,
                  shape: BoxShape.circle,
                ),
                child: RotationTransition(
                  turns: _controller,
                  child: Icon(
                    Icons.refresh,
                    size: 40,
                    color: Theme.of(context).colorScheme.onPrimaryContainer,
                  ),
                ),
              ),
            ),
            const SizedBox(height: 24),
            Text(
              _isLoading ? l10n.processingText : l10n.readyText,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 32),
            ElevatedButton.icon(
              onPressed: _isLoading ? _stopLoading : _startLoading,
              icon: Icon(_isLoading ? Icons.stop : Icons.play_arrow),
              label: Text(_isLoading ? l10n.stopButton : l10n.startButton),
            ),
          ],
        ),
      ),
    );
  }
}

ARB File Structure for RotationTransition

{
  "loadingDemoTitle": "Loading Demo",
  "@loadingDemoTitle": {
    "description": "Title for loading demo screen"
  },
  "loadingInProgress": "Loading in progress",
  "@loadingInProgress": {
    "description": "Accessibility label when loading"
  },
  "loadingIdle": "Ready to load",
  "@loadingIdle": {
    "description": "Accessibility label when idle"
  },
  "processingText": "Processing...",
  "@processingText": {
    "description": "Text shown during processing"
  },
  "readyText": "Ready",
  "@readyText": {
    "description": "Text shown when ready"
  },
  "stopButton": "Stop",
  "@stopButton": {
    "description": "Button to stop loading"
  },
  "startButton": "Start",
  "@startButton": {
    "description": "Button to start loading"
  }
}

RTL-Aware Directional Arrows

Handle directional arrows that respect text direction:

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

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

  @override
  State<LocalizedDirectionalArrows> createState() => _LocalizedDirectionalArrowsState();
}

class _LocalizedDirectionalArrowsState extends State<LocalizedDirectionalArrows>
    with TickerProviderStateMixin {
  late AnimationController _forwardController;
  late AnimationController _backController;
  int _currentPage = 0;
  static const int _totalPages = 5;

  @override
  void initState() {
    super.initState();
    _forwardController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _backController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  void _goForward() {
    if (_currentPage < _totalPages - 1) {
      _forwardController.forward().then((_) {
        _forwardController.reverse();
      });
      setState(() => _currentPage++);
    }
  }

  void _goBack() {
    if (_currentPage > 0) {
      _backController.forward().then((_) {
        _backController.reverse();
      });
      setState(() => _currentPage--);
    }
  }

  @override
  void dispose() {
    _forwardController.dispose();
    _backController.dispose();
    super.dispose();
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.paginationTitle)),
      body: Column(
        children: [
          Expanded(
            child: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    l10n.pageDisplay(_currentPage + 1, _totalPages),
                    style: Theme.of(context).textTheme.headlineLarge,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    l10n.pageContentExample(_currentPage + 1),
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                ],
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(24),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                // Back button
                Semantics(
                  button: true,
                  enabled: _currentPage > 0,
                  label: l10n.previousPageAccessibility,
                  child: IconButton.filled(
                    onPressed: _currentPage > 0 ? _goBack : null,
                    icon: RotationTransition(
                      turns: Tween<double>(
                        begin: 0,
                        end: isRtl ? 0.1 : -0.1,
                      ).animate(_backController),
                      child: Icon(
                        isRtl ? Icons.arrow_forward : Icons.arrow_back,
                      ),
                    ),
                  ),
                ),
                // Page indicators
                Row(
                  children: List.generate(_totalPages, (index) {
                    return Semantics(
                      label: l10n.pageIndicator(index + 1),
                      selected: index == _currentPage,
                      child: AnimatedContainer(
                        duration: const Duration(milliseconds: 200),
                        margin: const EdgeInsets.symmetric(horizontal: 4),
                        width: index == _currentPage ? 12 : 8,
                        height: index == _currentPage ? 12 : 8,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: index == _currentPage
                              ? Theme.of(context).colorScheme.primary
                              : Theme.of(context).colorScheme.outline,
                        ),
                      ),
                    );
                  }),
                ),
                // Forward button
                Semantics(
                  button: true,
                  enabled: _currentPage < _totalPages - 1,
                  label: l10n.nextPageAccessibility,
                  child: IconButton.filled(
                    onPressed: _currentPage < _totalPages - 1 ? _goForward : null,
                    icon: RotationTransition(
                      turns: Tween<double>(
                        begin: 0,
                        end: isRtl ? -0.1 : 0.1,
                      ).animate(_forwardController),
                      child: Icon(
                        isRtl ? Icons.arrow_back : Icons.arrow_forward,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Expandable Section with Rotating Icon

Create expandable sections with rotating arrow indicators:

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

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

  @override
  State<LocalizedExpandableSection> createState() => _LocalizedExpandableSectionState();
}

class _LocalizedExpandableSectionState extends State<LocalizedExpandableSection>
    with TickerProviderStateMixin {
  final Map<int, AnimationController> _controllers = {};
  final Map<int, bool> _expandedStates = {};

  @override
  void dispose() {
    for (var controller in _controllers.values) {
      controller.dispose();
    }
    super.dispose();
  }

  AnimationController _getController(int index) {
    if (!_controllers.containsKey(index)) {
      _controllers[index] = AnimationController(
        vsync: this,
        duration: const Duration(milliseconds: 200),
      );
      _expandedStates[index] = false;
    }
    return _controllers[index]!;
  }

  void _toggleSection(int index) {
    final controller = _getController(index);
    final isExpanded = _expandedStates[index] ?? false;

    setState(() {
      _expandedStates[index] = !isExpanded;
      if (!isExpanded) {
        controller.forward();
      } else {
        controller.reverse();
      }
    });
  }

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

    final sections = [
      (l10n.faqSection1Title, l10n.faqSection1Content, Icons.payment),
      (l10n.faqSection2Title, l10n.faqSection2Content, Icons.local_shipping),
      (l10n.faqSection3Title, l10n.faqSection3Content, Icons.assignment_return),
      (l10n.faqSection4Title, l10n.faqSection4Content, Icons.support_agent),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.faqTitle)),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: sections.length,
        itemBuilder: (context, index) {
          final section = sections[index];
          final controller = _getController(index);
          final isExpanded = _expandedStates[index] ?? false;

          return Card(
            margin: const EdgeInsets.only(bottom: 8),
            clipBehavior: Clip.antiAlias,
            child: Column(
              children: [
                InkWell(
                  onTap: () => _toggleSection(index),
                  child: Semantics(
                    button: true,
                    expanded: isExpanded,
                    label: l10n.faqSectionAccessibility(
                      section.$1,
                      isExpanded ? l10n.expanded : l10n.collapsed,
                    ),
                    child: Padding(
                      padding: const EdgeInsets.all(16),
                      child: Row(
                        children: [
                          Container(
                            padding: const EdgeInsets.all(8),
                            decoration: BoxDecoration(
                              color: Theme.of(context).colorScheme.primaryContainer,
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: Icon(
                              section.$3,
                              color: Theme.of(context).colorScheme.onPrimaryContainer,
                            ),
                          ),
                          const SizedBox(width: 16),
                          Expanded(
                            child: Text(
                              section.$1,
                              style: Theme.of(context).textTheme.titleMedium,
                            ),
                          ),
                          RotationTransition(
                            turns: Tween<double>(
                              begin: 0,
                              end: 0.5,
                            ).animate(CurvedAnimation(
                              parent: controller,
                              curve: Curves.easeOut,
                            )),
                            child: Icon(
                              Icons.keyboard_arrow_down,
                              color: Theme.of(context).colorScheme.primary,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
                AnimatedCrossFade(
                  firstChild: const SizedBox(width: double.infinity),
                  secondChild: Padding(
                    padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
                    child: Text(
                      section.$2,
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                    ),
                  ),
                  crossFadeState: isExpanded
                      ? CrossFadeState.showSecond
                      : CrossFadeState.showFirst,
                  duration: const Duration(milliseconds: 200),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

Sync Status Indicator

Create a sync status indicator with rotation animation:

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

enum SyncStatus { idle, syncing, success, error }

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

  @override
  State<LocalizedSyncIndicator> createState() => _LocalizedSyncIndicatorState();
}

class _LocalizedSyncIndicatorState extends State<LocalizedSyncIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  SyncStatus _status = SyncStatus.idle;
  DateTime? _lastSync;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 800),
    );
  }

  Future<void> _startSync() async {
    setState(() => _status = SyncStatus.syncing);
    _controller.repeat();

    await Future.delayed(const Duration(seconds: 2));

    _controller.stop();
    setState(() {
      _status = SyncStatus.success;
      _lastSync = DateTime.now();
    });

    await Future.delayed(const Duration(seconds: 2));
    setState(() => _status = SyncStatus.idle);
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.syncStatusTitle)),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Card(
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  children: [
                    Semantics(
                      liveRegion: true,
                      label: _getStatusAccessibility(l10n),
                      child: Container(
                        width: 100,
                        height: 100,
                        decoration: BoxDecoration(
                          color: _getStatusColor(context).withOpacity(0.2),
                          shape: BoxShape.circle,
                        ),
                        child: Center(
                          child: _status == SyncStatus.syncing
                              ? RotationTransition(
                                  turns: _controller,
                                  child: Icon(
                                    Icons.sync,
                                    size: 48,
                                    color: _getStatusColor(context),
                                  ),
                                )
                              : Icon(
                                  _getStatusIcon(),
                                  size: 48,
                                  color: _getStatusColor(context),
                                ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 24),
                    Text(
                      _getStatusText(l10n),
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    if (_lastSync != null) ...[
                      const SizedBox(height: 8),
                      Text(
                        l10n.lastSyncTime(_formatTime(_lastSync!)),
                        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                          color: Theme.of(context).colorScheme.outline,
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton.icon(
                onPressed: _status == SyncStatus.syncing ? null : _startSync,
                icon: const Icon(Icons.sync),
                label: Text(l10n.syncNowButton),
              ),
            ),
            const SizedBox(height: 32),
            _SyncItem(
              icon: Icons.contacts,
              title: l10n.contactsSyncTitle,
              status: _status == SyncStatus.syncing
                  ? l10n.syncingStatus
                  : l10n.upToDateStatus,
              isSyncing: _status == SyncStatus.syncing,
            ),
            _SyncItem(
              icon: Icons.calendar_today,
              title: l10n.calendarSyncTitle,
              status: _status == SyncStatus.syncing
                  ? l10n.syncingStatus
                  : l10n.upToDateStatus,
              isSyncing: _status == SyncStatus.syncing,
            ),
            _SyncItem(
              icon: Icons.folder,
              title: l10n.filesSyncTitle,
              status: _status == SyncStatus.syncing
                  ? l10n.syncingStatus
                  : l10n.upToDateStatus,
              isSyncing: _status == SyncStatus.syncing,
            ),
          ],
        ),
      ),
    );
  }

  Color _getStatusColor(BuildContext context) {
    return switch (_status) {
      SyncStatus.idle => Theme.of(context).colorScheme.outline,
      SyncStatus.syncing => Theme.of(context).colorScheme.primary,
      SyncStatus.success => Colors.green,
      SyncStatus.error => Theme.of(context).colorScheme.error,
    };
  }

  IconData _getStatusIcon() {
    return switch (_status) {
      SyncStatus.idle => Icons.sync,
      SyncStatus.syncing => Icons.sync,
      SyncStatus.success => Icons.check_circle,
      SyncStatus.error => Icons.error,
    };
  }

  String _getStatusText(AppLocalizations l10n) {
    return switch (_status) {
      SyncStatus.idle => l10n.syncStatusIdle,
      SyncStatus.syncing => l10n.syncStatusSyncing,
      SyncStatus.success => l10n.syncStatusSuccess,
      SyncStatus.error => l10n.syncStatusError,
    };
  }

  String _getStatusAccessibility(AppLocalizations l10n) {
    return switch (_status) {
      SyncStatus.idle => l10n.syncAccessibilityIdle,
      SyncStatus.syncing => l10n.syncAccessibilitySyncing,
      SyncStatus.success => l10n.syncAccessibilitySuccess,
      SyncStatus.error => l10n.syncAccessibilityError,
    };
  }

  String _formatTime(DateTime time) {
    return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
  }
}

class _SyncItem extends StatelessWidget {
  final IconData icon;
  final String title;
  final String status;
  final bool isSyncing;

  const _SyncItem({
    required this.icon,
    required this.title,
    required this.status,
    required this.isSyncing,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: Icon(icon),
        title: Text(title),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              status,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: Theme.of(context).colorScheme.outline,
              ),
            ),
            const SizedBox(width: 8),
            if (isSyncing)
              SizedBox(
                width: 16,
                height: 16,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                  color: Theme.of(context).colorScheme.primary,
                ),
              )
            else
              Icon(
                Icons.check_circle,
                size: 16,
                color: Colors.green,
              ),
          ],
        ),
      ),
    );
  }
}

Settings Wheel with Rotation

Create an animated settings button with rotation:

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

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

  @override
  State<LocalizedSettingsWheel> createState() => _LocalizedSettingsWheelState();
}

class _LocalizedSettingsWheelState extends State<LocalizedSettingsWheel>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool _isSettingsOpen = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );
  }

  void _toggleSettings() {
    setState(() {
      _isSettingsOpen = !_isSettingsOpen;
      if (_isSettingsOpen) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

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

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.settingsWheelTitle),
        actions: [
          Semantics(
            button: true,
            label: _isSettingsOpen
                ? l10n.closeSettingsAccessibility
                : l10n.openSettingsAccessibility,
            child: IconButton(
              onPressed: _toggleSettings,
              icon: RotationTransition(
                turns: Tween<double>(
                  begin: 0,
                  end: 0.5,
                ).animate(CurvedAnimation(
                  parent: _controller,
                  curve: Curves.easeInOut,
                )),
                child: Icon(
                  _isSettingsOpen ? Icons.close : Icons.settings,
                ),
              ),
            ),
          ),
        ],
      ),
      body: Stack(
        children: [
          Center(
            child: Text(l10n.mainContentLabel),
          ),
          AnimatedPositioned(
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeOutCubic,
            top: 0,
            right: 0,
            left: 0,
            height: _isSettingsOpen ? 300 : 0,
            child: Material(
              elevation: 4,
              child: SingleChildScrollView(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      l10n.quickSettingsHeader,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 16),
                    _SettingsOption(
                      icon: Icons.dark_mode,
                      title: l10n.darkModeOption,
                      value: true,
                      onChanged: (value) {},
                    ),
                    _SettingsOption(
                      icon: Icons.notifications,
                      title: l10n.notificationsOption,
                      value: true,
                      onChanged: (value) {},
                    ),
                    _SettingsOption(
                      icon: Icons.location_on,
                      title: l10n.locationOption,
                      value: false,
                      onChanged: (value) {},
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _SettingsOption extends StatelessWidget {
  final IconData icon;
  final String title;
  final bool value;
  final ValueChanged<bool> onChanged;

  const _SettingsOption({
    required this.icon,
    required this.title,
    required this.value,
    required this.onChanged,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      trailing: Switch(
        value: value,
        onChanged: onChanged,
      ),
    );
  }
}

Coin Flip Animation

Create a coin flip animation with localized outcomes:

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

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

  @override
  State<LocalizedCoinFlip> createState() => _LocalizedCoinFlipState();
}

class _LocalizedCoinFlipState extends State<LocalizedCoinFlip>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool? _isHeads;
  bool _isFlipping = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1500),
    );

    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        setState(() {
          _isFlipping = false;
          _isHeads = Random().nextBool();
        });
      }
    });
  }

  void _flipCoin() {
    if (_isFlipping) return;

    setState(() {
      _isFlipping = true;
      _isHeads = null;
    });

    _controller.reset();
    _controller.forward();
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.coinFlipTitle)),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Semantics(
              liveRegion: true,
              label: _isFlipping
                  ? l10n.coinFlippingAccessibility
                  : _isHeads == null
                      ? l10n.coinReadyAccessibility
                      : _isHeads!
                          ? l10n.coinHeadsAccessibility
                          : l10n.coinTailsAccessibility,
              child: RotationTransition(
                turns: Tween<double>(
                  begin: 0,
                  end: 5,
                ).animate(CurvedAnimation(
                  parent: _controller,
                  curve: Curves.easeOutCubic,
                )),
                child: Container(
                  width: 150,
                  height: 150,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    gradient: LinearGradient(
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                      colors: [
                        Colors.amber.shade300,
                        Colors.amber.shade700,
                      ],
                    ),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.amber.withOpacity(0.5),
                        blurRadius: 20,
                        spreadRadius: 2,
                      ),
                    ],
                  ),
                  child: Center(
                    child: _isFlipping
                        ? null
                        : Text(
                            _isHeads == null
                                ? '?'
                                : _isHeads!
                                    ? l10n.headsSymbol
                                    : l10n.tailsSymbol,
                            style: Theme.of(context).textTheme.headlineLarge?.copyWith(
                              color: Colors.amber.shade900,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 32),
            AnimatedSwitcher(
              duration: const Duration(milliseconds: 200),
              child: Text(
                _isFlipping
                    ? l10n.flippingText
                    : _isHeads == null
                        ? l10n.tapToFlipText
                        : _isHeads!
                            ? l10n.headsResultText
                            : l10n.tailsResultText,
                key: ValueKey(_isFlipping ? 'flipping' : _isHeads),
                style: Theme.of(context).textTheme.headlineSmall,
              ),
            ),
            const SizedBox(height: 32),
            ElevatedButton.icon(
              onPressed: _isFlipping ? null : _flipCoin,
              icon: const Icon(Icons.refresh),
              label: Text(l10n.flipCoinButton),
            ),
          ],
        ),
      ),
    );
  }
}

Complete ARB File for RotationTransition

{
  "@@locale": "en",

  "loadingDemoTitle": "Loading Demo",
  "loadingInProgress": "Loading in progress",
  "loadingIdle": "Ready to load",
  "processingText": "Processing...",
  "readyText": "Ready",
  "stopButton": "Stop",
  "startButton": "Start",

  "paginationTitle": "Pagination",
  "pageDisplay": "Page {current} of {total}",
  "@pageDisplay": {
    "placeholders": {
      "current": {"type": "int"},
      "total": {"type": "int"}
    }
  },
  "pageContentExample": "This is the content for page {number}",
  "@pageContentExample": {
    "placeholders": {"number": {"type": "int"}}
  },
  "previousPageAccessibility": "Go to previous page",
  "nextPageAccessibility": "Go to next page",
  "pageIndicator": "Page {number}",
  "@pageIndicator": {
    "placeholders": {"number": {"type": "int"}}
  },

  "faqTitle": "Frequently Asked Questions",
  "faqSection1Title": "Payment Methods",
  "faqSection1Content": "We accept all major credit cards, PayPal, and bank transfers. Your payment information is encrypted and secure.",
  "faqSection2Title": "Shipping Information",
  "faqSection2Content": "We offer free shipping on orders over $50. Standard delivery takes 3-5 business days.",
  "faqSection3Title": "Returns & Refunds",
  "faqSection3Content": "Items can be returned within 30 days of purchase. Refunds are processed within 5-7 business days.",
  "faqSection4Title": "Customer Support",
  "faqSection4Content": "Our support team is available 24/7. You can reach us via email, phone, or live chat.",
  "faqSectionAccessibility": "{title}, {state}",
  "@faqSectionAccessibility": {
    "placeholders": {
      "title": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "expanded": "expanded",
  "collapsed": "collapsed",

  "syncStatusTitle": "Sync Status",
  "lastSyncTime": "Last sync: {time}",
  "@lastSyncTime": {
    "placeholders": {"time": {"type": "String"}}
  },
  "syncNowButton": "Sync Now",
  "syncStatusIdle": "Ready to sync",
  "syncStatusSyncing": "Syncing...",
  "syncStatusSuccess": "Sync complete",
  "syncStatusError": "Sync failed",
  "syncAccessibilityIdle": "Sync status: ready",
  "syncAccessibilitySyncing": "Sync status: syncing in progress",
  "syncAccessibilitySuccess": "Sync status: completed successfully",
  "syncAccessibilityError": "Sync status: failed",
  "contactsSyncTitle": "Contacts",
  "calendarSyncTitle": "Calendar",
  "filesSyncTitle": "Files",
  "syncingStatus": "Syncing...",
  "upToDateStatus": "Up to date",

  "settingsWheelTitle": "Settings",
  "openSettingsAccessibility": "Open settings menu",
  "closeSettingsAccessibility": "Close settings menu",
  "mainContentLabel": "Main content area",
  "quickSettingsHeader": "Quick Settings",
  "darkModeOption": "Dark Mode",
  "notificationsOption": "Notifications",
  "locationOption": "Location Services",

  "coinFlipTitle": "Coin Flip",
  "coinFlippingAccessibility": "Coin is flipping",
  "coinReadyAccessibility": "Tap to flip the coin",
  "coinHeadsAccessibility": "Result: Heads",
  "coinTailsAccessibility": "Result: Tails",
  "headsSymbol": "H",
  "tailsSymbol": "T",
  "flippingText": "Flipping...",
  "tapToFlipText": "Tap to flip",
  "headsResultText": "Heads!",
  "tailsResultText": "Tails!",
  "flipCoinButton": "Flip Again"
}

Best Practices Summary

  1. Handle RTL direction: Adjust rotation direction for directional indicators in RTL layouts
  2. Use appropriate turn values: Full rotation (1.0), half (0.5), quarter (0.25)
  3. Provide accessibility labels: Announce rotation states to screen readers
  4. Choose meaningful rotation speeds: Faster for loading, slower for emphasis
  5. Combine with other transitions: Mix rotation with fade or scale for polish
  6. Test with different locales: Verify directional animations work correctly in RTL
  7. Use repeating animations: For continuous loading states with repeat()
  8. Add curve animations: Apply easing for natural-feeling rotations
  9. Handle state changes: Update accessibility labels when rotation state changes
  10. Maintain consistency: Use the same rotation patterns throughout your app

Conclusion

RotationTransition is a powerful widget for creating explicit rotation animations in multilingual Flutter apps. By properly handling RTL layouts, providing meaningful accessibility announcements, and choosing appropriate rotation values, you create intuitive experiences for users worldwide. The patterns shown here—loading indicators, directional arrows, expandable sections, and status indicators—can be adapted for any application requiring dynamic rotational animations.

Remember to test your rotation animations with various locales to ensure that directional indicators behave correctly regardless of the user's language preference.