← Back to Blog

Flutter TweenAnimationBuilder Localization: Animated Text, Counters, and Dynamic Values

fluttertweenanimationbuilderanimationcounterslocalizationrtl

Flutter TweenAnimationBuilder Localization: Animated Text, Counters, and Dynamic Values

TweenAnimationBuilder provides a powerful way to animate values without managing AnimationControllers. Proper localization ensures animated text, counters, and dynamic values display correctly across languages. This guide covers comprehensive strategies for localizing TweenAnimationBuilder widgets in Flutter.

Understanding TweenAnimationBuilder Localization

TweenAnimationBuilder widgets require localization for:

  • Animated counters: Numbers that increment with proper formatting
  • Progress labels: Percentage text that updates smoothly
  • Animated text: Labels that fade or scale during transitions
  • Dynamic values: Currency, measurements, and formatted numbers
  • Accessibility labels: Screen reader text for animated changes
  • RTL animations: Direction-aware animation flows

Basic Counter Animation with Localized Numbers

Start with a simple animated counter using locale-aware formatting:

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

class LocalizedAnimatedCounter extends StatefulWidget {
  final int targetValue;
  final Duration duration;
  final String? prefix;
  final String? suffix;

  const LocalizedAnimatedCounter({
    super.key,
    required this.targetValue,
    this.duration = const Duration(seconds: 2),
    this.prefix,
    this.suffix,
  });

  @override
  State<LocalizedAnimatedCounter> createState() =>
      _LocalizedAnimatedCounterState();
}

class _LocalizedAnimatedCounterState extends State<LocalizedAnimatedCounter> {
  late int _previousValue;
  late int _currentTarget;

  @override
  void initState() {
    super.initState();
    _previousValue = 0;
    _currentTarget = widget.targetValue;
  }

  @override
  void didUpdateWidget(LocalizedAnimatedCounter oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.targetValue != widget.targetValue) {
      _previousValue = oldWidget.targetValue;
      _currentTarget = widget.targetValue;
    }
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);
    final numberFormat = NumberFormat.decimalPattern(locale.toString());

    return TweenAnimationBuilder<int>(
      tween: IntTween(begin: _previousValue, end: _currentTarget),
      duration: widget.duration,
      curve: Curves.easeOutCubic,
      builder: (context, value, child) {
        final formattedValue = numberFormat.format(value);

        return Semantics(
          label: l10n.counterValue(formattedValue),
          liveRegion: true,
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (widget.prefix != null)
                Text(
                  widget.prefix!,
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              Text(
                formattedValue,
                style: Theme.of(context).textTheme.headlineLarge?.copyWith(
                  fontWeight: FontWeight.bold,
                  fontFeatures: const [FontFeature.tabularFigures()],
                ),
              ),
              if (widget.suffix != null)
                Text(
                  widget.suffix!,
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
            ],
          ),
        );
      },
    );
  }
}

// Usage example
class StatsScreen extends StatelessWidget {
  const StatsScreen({super.key});

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

    return Column(
      children: [
        LocalizedAnimatedCounter(
          targetValue: 1234567,
          prefix: l10n.currencySymbol,
        ),
        LocalizedAnimatedCounter(
          targetValue: 98765,
          suffix: ' ${l10n.usersLabel}',
        ),
      ],
    );
  }
}

Animated Progress with Localized Labels

Create progress indicators with animated percentage text:

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

class LocalizedAnimatedProgress extends StatelessWidget {
  final double progress;
  final String? label;
  final Duration duration;
  final Color? progressColor;
  final Color? backgroundColor;

  const LocalizedAnimatedProgress({
    super.key,
    required this.progress,
    this.label,
    this.duration = const Duration(milliseconds: 800),
    this.progressColor,
    this.backgroundColor,
  });

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);
    final percentFormat = NumberFormat.percentPattern(locale.toString());

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              label ?? l10n.progress,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            TweenAnimationBuilder<double>(
              tween: Tween(begin: 0, end: progress),
              duration: duration,
              curve: Curves.easeOutCubic,
              builder: (context, value, child) {
                return Semantics(
                  label: l10n.progressPercentage(
                    percentFormat.format(value),
                  ),
                  child: Text(
                    percentFormat.format(value),
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                );
              },
            ),
          ],
        ),
        const SizedBox(height: 8),
        TweenAnimationBuilder<double>(
          tween: Tween(begin: 0, end: progress),
          duration: duration,
          curve: Curves.easeOutCubic,
          builder: (context, value, child) {
            return LinearProgressIndicator(
              value: value,
              backgroundColor: backgroundColor ??
                  Theme.of(context).colorScheme.surfaceVariant,
              valueColor: AlwaysStoppedAnimation(
                progressColor ?? Theme.of(context).colorScheme.primary,
              ),
              minHeight: 8,
              borderRadius: BorderRadius.circular(4),
            );
          },
        ),
      ],
    );
  }
}

// Multi-progress tracker with localized labels
class LocalizedProgressTracker extends StatelessWidget {
  final List<ProgressItem> items;

  const LocalizedProgressTracker({super.key, required this.items});

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

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l10n.progressTrackerTitle,
          style: Theme.of(context).textTheme.titleLarge,
        ),
        const SizedBox(height: 16),
        ...items.asMap().entries.map((entry) {
          final index = entry.key;
          final item = entry.value;

          return Padding(
            padding: const EdgeInsets.only(bottom: 16),
            child: LocalizedAnimatedProgress(
              progress: item.progress,
              label: item.label,
              progressColor: item.color,
              duration: Duration(milliseconds: 600 + (index * 200)),
            ),
          );
        }),
        const Divider(),
        _buildTotalProgress(context, l10n),
      ],
    );
  }

  Widget _buildTotalProgress(BuildContext context, AppLocalizations l10n) {
    final total = items.fold<double>(
      0,
      (sum, item) => sum + item.progress,
    ) / items.length;

    return LocalizedAnimatedProgress(
      progress: total,
      label: l10n.overallProgress,
      progressColor: Theme.of(context).colorScheme.secondary,
      duration: const Duration(seconds: 1),
    );
  }
}

class ProgressItem {
  final String label;
  final double progress;
  final Color? color;

  ProgressItem({
    required this.label,
    required this.progress,
    this.color,
  });
}

Animated Currency with Locale Formatting

Display currency values with smooth animations:

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

class LocalizedAnimatedCurrency extends StatefulWidget {
  final double value;
  final String currencyCode;
  final Duration duration;
  final TextStyle? style;
  final bool showChange;

  const LocalizedAnimatedCurrency({
    super.key,
    required this.value,
    required this.currencyCode,
    this.duration = const Duration(milliseconds: 1500),
    this.style,
    this.showChange = false,
  });

  @override
  State<LocalizedAnimatedCurrency> createState() =>
      _LocalizedAnimatedCurrencyState();
}

class _LocalizedAnimatedCurrencyState extends State<LocalizedAnimatedCurrency> {
  late double _previousValue;

  @override
  void initState() {
    super.initState();
    _previousValue = 0;
  }

  @override
  void didUpdateWidget(LocalizedAnimatedCurrency oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.value != widget.value) {
      _previousValue = oldWidget.value;
    }
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);
    final currencyFormat = NumberFormat.currency(
      locale: locale.toString(),
      symbol: widget.currencyCode,
      decimalDigits: 2,
    );

    return TweenAnimationBuilder<double>(
      tween: Tween(begin: _previousValue, end: widget.value),
      duration: widget.duration,
      curve: Curves.easeOutExpo,
      builder: (context, value, child) {
        final change = value - _previousValue;
        final isPositive = change >= 0;

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Semantics(
              label: l10n.currencyValue(currencyFormat.format(value)),
              child: Text(
                currencyFormat.format(value),
                style: widget.style ??
                    Theme.of(context).textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                      fontFeatures: const [FontFeature.tabularFigures()],
                    ),
              ),
            ),
            if (widget.showChange && _previousValue != 0) ...[
              const SizedBox(height: 4),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    isPositive ? Icons.arrow_upward : Icons.arrow_downward,
                    size: 16,
                    color: isPositive ? Colors.green : Colors.red,
                  ),
                  Text(
                    l10n.currencyChange(
                      isPositive ? '+' : '',
                      currencyFormat.format(change.abs()),
                    ),
                    style: TextStyle(
                      color: isPositive ? Colors.green : Colors.red,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ],
              ),
            ],
          ],
        );
      },
    );
  }
}

// Dashboard with animated financial stats
class LocalizedFinancialDashboard extends StatelessWidget {
  final double balance;
  final double income;
  final double expenses;
  final String currencyCode;

  const LocalizedFinancialDashboard({
    super.key,
    required this.balance,
    required this.income,
    required this.expenses,
    required this.currencyCode,
  });

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

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.financialOverview,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 24),
            // Balance
            Text(
              l10n.currentBalance,
              style: Theme.of(context).textTheme.bodySmall,
            ),
            LocalizedAnimatedCurrency(
              value: balance,
              currencyCode: currencyCode,
              showChange: true,
            ),
            const SizedBox(height: 24),
            Row(
              children: [
                // Income
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          const Icon(Icons.arrow_downward,
                              color: Colors.green, size: 16),
                          const SizedBox(width: 4),
                          Text(l10n.income),
                        ],
                      ),
                      LocalizedAnimatedCurrency(
                        value: income,
                        currencyCode: currencyCode,
                        style: Theme.of(context).textTheme.titleMedium?.copyWith(
                          color: Colors.green,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
                // Expenses
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          const Icon(Icons.arrow_upward,
                              color: Colors.red, size: 16),
                          const SizedBox(width: 4),
                          Text(l10n.expenses),
                        ],
                      ),
                      LocalizedAnimatedCurrency(
                        value: expenses,
                        currencyCode: currencyCode,
                        style: Theme.of(context).textTheme.titleMedium?.copyWith(
                          color: Colors.red,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Animated Text Transitions with RTL Support

Handle text transitions that respect reading direction:

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

class LocalizedAnimatedText extends StatelessWidget {
  final String text;
  final Duration duration;
  final TextStyle? style;
  final AnimatedTextType type;

  const LocalizedAnimatedText({
    super.key,
    required this.text,
    this.duration = const Duration(milliseconds: 500),
    this.style,
    this.type = AnimatedTextType.fadeSlide,
  });

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

    return switch (type) {
      AnimatedTextType.fade => _buildFadeText(context),
      AnimatedTextType.scale => _buildScaleText(context),
      AnimatedTextType.fadeSlide => _buildFadeSlideText(context, isRtl),
      AnimatedTextType.typewriter => _buildTypewriterText(context),
    };
  }

  Widget _buildFadeText(BuildContext context) {
    return TweenAnimationBuilder<double>(
      key: ValueKey(text),
      tween: Tween(begin: 0, end: 1),
      duration: duration,
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Text(text, style: style),
        );
      },
    );
  }

  Widget _buildScaleText(BuildContext context) {
    return TweenAnimationBuilder<double>(
      key: ValueKey(text),
      tween: Tween(begin: 0.8, end: 1),
      duration: duration,
      curve: Curves.elasticOut,
      builder: (context, value, child) {
        return Transform.scale(
          scale: value,
          child: Text(text, style: style),
        );
      },
    );
  }

  Widget _buildFadeSlideText(BuildContext context, bool isRtl) {
    // Adjust slide direction based on RTL
    final slideOffset = isRtl ? -20.0 : 20.0;

    return TweenAnimationBuilder<double>(
      key: ValueKey(text),
      tween: Tween(begin: 0, end: 1),
      duration: duration,
      curve: Curves.easeOutCubic,
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.translate(
            offset: Offset(slideOffset * (1 - value), 0),
            child: Text(text, style: style),
          ),
        );
      },
    );
  }

  Widget _buildTypewriterText(BuildContext context) {
    return TweenAnimationBuilder<int>(
      key: ValueKey(text),
      tween: IntTween(begin: 0, end: text.length),
      duration: Duration(milliseconds: text.length * 50),
      builder: (context, value, child) {
        return Text(
          text.substring(0, value),
          style: style,
        );
      },
    );
  }
}

enum AnimatedTextType {
  fade,
  scale,
  fadeSlide,
  typewriter,
}

// Status message with animated transitions
class LocalizedStatusMessage extends StatelessWidget {
  final StatusType status;

  const LocalizedStatusMessage({super.key, required this.status});

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

    final (message, icon, color) = switch (status) {
      StatusType.loading => (
          l10n.statusLoading,
          Icons.hourglass_empty,
          Colors.blue
        ),
      StatusType.success => (l10n.statusSuccess, Icons.check_circle, Colors.green),
      StatusType.error => (l10n.statusError, Icons.error, Colors.red),
      StatusType.warning => (l10n.statusWarning, Icons.warning, Colors.orange),
    };

    return TweenAnimationBuilder<double>(
      key: ValueKey(status),
      tween: Tween(begin: 0, end: 1),
      duration: const Duration(milliseconds: 400),
      curve: Curves.easeOutBack,
      builder: (context, value, child) {
        return Transform.scale(
          scale: value,
          child: Opacity(
            opacity: value,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
              decoration: BoxDecoration(
                color: color.withOpacity(0.1),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: color.withOpacity(0.3)),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(icon, color: color, size: 20),
                  const SizedBox(width: 8),
                  Text(
                    message,
                    style: TextStyle(
                      color: color,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

enum StatusType { loading, success, error, warning }

Animated List Items with Staggered Delays

Create staggered animations for list content:

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

class LocalizedAnimatedStatsList extends StatelessWidget {
  final List<StatItem> items;

  const LocalizedAnimatedStatsList({super.key, required this.items});

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

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l10n.statisticsTitle,
          style: Theme.of(context).textTheme.titleLarge,
        ),
        const SizedBox(height: 16),
        ...items.asMap().entries.map((entry) {
          final index = entry.key;
          final item = entry.value;
          final delay = Duration(milliseconds: index * 150);

          return _AnimatedStatItem(
            item: item,
            delay: delay,
            locale: locale,
            isRtl: isRtl,
          );
        }),
      ],
    );
  }
}

class _AnimatedStatItem extends StatefulWidget {
  final StatItem item;
  final Duration delay;
  final Locale locale;
  final bool isRtl;

  const _AnimatedStatItem({
    required this.item,
    required this.delay,
    required this.locale,
    required this.isRtl,
  });

  @override
  State<_AnimatedStatItem> createState() => _AnimatedStatItemState();
}

class _AnimatedStatItemState extends State<_AnimatedStatItem> {
  bool _animate = false;

  @override
  void initState() {
    super.initState();
    Future.delayed(widget.delay, () {
      if (mounted) {
        setState(() => _animate = true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final numberFormat = NumberFormat.compact(locale: widget.locale.toString());
    final slideOffset = widget.isRtl ? -30.0 : 30.0;

    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: _animate ? 1.0 : 0.0),
      duration: const Duration(milliseconds: 600),
      curve: Curves.easeOutCubic,
      builder: (context, animation, child) {
        return Transform.translate(
          offset: Offset(slideOffset * (1 - animation), 0),
          child: Opacity(
            opacity: animation,
            child: Card(
              margin: const EdgeInsets.only(bottom: 8),
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    Container(
                      width: 48,
                      height: 48,
                      decoration: BoxDecoration(
                        color: widget.item.color.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: Icon(
                        widget.item.icon,
                        color: widget.item.color,
                      ),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            widget.item.label,
                            style: Theme.of(context).textTheme.bodyMedium,
                          ),
                          TweenAnimationBuilder<double>(
                            tween: Tween(
                              begin: 0,
                              end: _animate ? widget.item.value : 0,
                            ),
                            duration: Duration(
                              milliseconds: 800 + (widget.delay.inMilliseconds),
                            ),
                            curve: Curves.easeOutCubic,
                            builder: (context, value, child) {
                              return Text(
                                _formatValue(numberFormat, value, widget.item),
                                style: Theme.of(context)
                                    .textTheme
                                    .headlineSmall
                                    ?.copyWith(
                                      fontWeight: FontWeight.bold,
                                      color: widget.item.color,
                                    ),
                              );
                            },
                          ),
                        ],
                      ),
                    ),
                    if (widget.item.trend != null)
                      _buildTrendIndicator(l10n, widget.item.trend!),
                  ],
                ),
              ),
            ),
          ),
        );
      },
    );
  }

  String _formatValue(NumberFormat format, double value, StatItem item) {
    if (item.isPercentage) {
      return '${value.toStringAsFixed(1)}%';
    } else if (item.isCurrency) {
      return '\$${format.format(value.toInt())}';
    }
    return format.format(value.toInt());
  }

  Widget _buildTrendIndicator(AppLocalizations l10n, double trend) {
    final isPositive = trend >= 0;

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: (isPositive ? Colors.green : Colors.red).withOpacity(0.1),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            isPositive ? Icons.trending_up : Icons.trending_down,
            size: 16,
            color: isPositive ? Colors.green : Colors.red,
          ),
          const SizedBox(width: 4),
          Text(
            l10n.trendPercentage(
              isPositive ? '+' : '',
              trend.abs().toStringAsFixed(1),
            ),
            style: TextStyle(
              color: isPositive ? Colors.green : Colors.red,
              fontWeight: FontWeight.w500,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }
}

class StatItem {
  final String label;
  final double value;
  final IconData icon;
  final Color color;
  final double? trend;
  final bool isPercentage;
  final bool isCurrency;

  StatItem({
    required this.label,
    required this.value,
    required this.icon,
    required this.color,
    this.trend,
    this.isPercentage = false,
    this.isCurrency = false,
  });
}

Circular Progress with Localized Center Text

Animated circular progress with localized content:

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

class LocalizedCircularProgress extends StatelessWidget {
  final double progress;
  final double size;
  final double strokeWidth;
  final String? label;
  final Duration duration;
  final Color? progressColor;
  final Color? backgroundColor;

  const LocalizedCircularProgress({
    super.key,
    required this.progress,
    this.size = 120,
    this.strokeWidth = 10,
    this.label,
    this.duration = const Duration(milliseconds: 1500),
    this.progressColor,
    this.backgroundColor,
  });

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);
    final percentFormat = NumberFormat.percentPattern(locale.toString());

    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: progress),
      duration: duration,
      curve: Curves.easeOutCubic,
      builder: (context, value, child) {
        return Semantics(
          label: l10n.progressAccessibility(percentFormat.format(value)),
          child: SizedBox(
            width: size,
            height: size,
            child: Stack(
              alignment: Alignment.center,
              children: [
                // Background circle
                SizedBox(
                  width: size,
                  height: size,
                  child: CircularProgressIndicator(
                    value: 1,
                    strokeWidth: strokeWidth,
                    valueColor: AlwaysStoppedAnimation(
                      backgroundColor ??
                          Theme.of(context).colorScheme.surfaceVariant,
                    ),
                  ),
                ),
                // Progress arc
                SizedBox(
                  width: size,
                  height: size,
                  child: CircularProgressIndicator(
                    value: value,
                    strokeWidth: strokeWidth,
                    valueColor: AlwaysStoppedAnimation(
                      progressColor ?? Theme.of(context).colorScheme.primary,
                    ),
                    strokeCap: StrokeCap.round,
                  ),
                ),
                // Center content
                Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(
                      percentFormat.format(value),
                      style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    if (label != null)
                      Text(
                        label!,
                        style: Theme.of(context).textTheme.bodySmall,
                      ),
                  ],
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

// Multi-ring progress for multiple metrics
class LocalizedMultiProgress extends StatelessWidget {
  final List<ProgressRing> rings;
  final double size;

  const LocalizedMultiProgress({
    super.key,
    required this.rings,
    this.size = 200,
  });

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

    return Column(
      children: [
        SizedBox(
          width: size,
          height: size,
          child: Stack(
            alignment: Alignment.center,
            children: rings.asMap().entries.map((entry) {
              final index = entry.key;
              final ring = entry.value;
              final ringSize = size - (index * 40);
              final delay = Duration(milliseconds: index * 200);

              return _DelayedRing(
                ring: ring,
                size: ringSize,
                delay: delay,
              );
            }).toList(),
          ),
        ),
        const SizedBox(height: 24),
        // Legend
        Wrap(
          spacing: 16,
          runSpacing: 8,
          children: rings.map((ring) {
            return Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Container(
                  width: 12,
                  height: 12,
                  decoration: BoxDecoration(
                    color: ring.color,
                    shape: BoxShape.circle,
                  ),
                ),
                const SizedBox(width: 8),
                Text(ring.label),
              ],
            );
          }).toList(),
        ),
      ],
    );
  }
}

class _DelayedRing extends StatefulWidget {
  final ProgressRing ring;
  final double size;
  final Duration delay;

  const _DelayedRing({
    required this.ring,
    required this.size,
    required this.delay,
  });

  @override
  State<_DelayedRing> createState() => _DelayedRingState();
}

class _DelayedRingState extends State<_DelayedRing> {
  bool _animate = false;

  @override
  void initState() {
    super.initState();
    Future.delayed(widget.delay, () {
      if (mounted) {
        setState(() => _animate = true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: _animate ? widget.ring.progress : 0),
      duration: const Duration(milliseconds: 1200),
      curve: Curves.easeOutCubic,
      builder: (context, value, child) {
        return SizedBox(
          width: widget.size,
          height: widget.size,
          child: CustomPaint(
            painter: _RingPainter(
              progress: value,
              color: widget.ring.color,
              strokeWidth: 8,
            ),
          ),
        );
      },
    );
  }
}

class _RingPainter extends CustomPainter {
  final double progress;
  final Color color;
  final double strokeWidth;

  _RingPainter({
    required this.progress,
    required this.color,
    required this.strokeWidth,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width - strokeWidth) / 2;

    // Background
    final bgPaint = Paint()
      ..color = color.withOpacity(0.2)
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth;

    canvas.drawCircle(center, radius, bgPaint);

    // Progress
    final progressPaint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -math.pi / 2,
      2 * math.pi * progress,
      false,
      progressPaint,
    );
  }

  @override
  bool shouldRepaint(_RingPainter oldDelegate) {
    return oldDelegate.progress != progress || oldDelegate.color != color;
  }
}

class ProgressRing {
  final String label;
  final double progress;
  final Color color;

  ProgressRing({
    required this.label,
    required this.progress,
    required this.color,
  });
}

Complete ARB File for TweenAnimationBuilder

{
  "@@locale": "en",

  "counterValue": "Value: {value}",
  "@counterValue": {
    "description": "Accessibility label for counter",
    "placeholders": {
      "value": {"type": "String"}
    }
  },
  "currencySymbol": "$",
  "@currencySymbol": {
    "description": "Currency symbol for display"
  },
  "usersLabel": "users",
  "@usersLabel": {
    "description": "Users suffix label"
  },

  "progress": "Progress",
  "@progress": {
    "description": "Generic progress label"
  },
  "progressPercentage": "{percent} complete",
  "@progressPercentage": {
    "description": "Progress percentage text",
    "placeholders": {
      "percent": {"type": "String"}
    }
  },
  "progressAccessibility": "Progress is at {percent}",
  "@progressAccessibility": {
    "description": "Accessibility label for progress",
    "placeholders": {
      "percent": {"type": "String"}
    }
  },
  "progressTrackerTitle": "Progress Tracker",
  "@progressTrackerTitle": {
    "description": "Title for progress tracker"
  },
  "overallProgress": "Overall Progress",
  "@overallProgress": {
    "description": "Label for total progress"
  },

  "currencyValue": "Amount: {value}",
  "@currencyValue": {
    "description": "Currency accessibility label",
    "placeholders": {
      "value": {"type": "String"}
    }
  },
  "currencyChange": "{sign}{amount}",
  "@currencyChange": {
    "description": "Currency change indicator",
    "placeholders": {
      "sign": {"type": "String"},
      "amount": {"type": "String"}
    }
  },
  "financialOverview": "Financial Overview",
  "@financialOverview": {
    "description": "Dashboard title"
  },
  "currentBalance": "Current Balance",
  "@currentBalance": {
    "description": "Balance label"
  },
  "income": "Income",
  "@income": {
    "description": "Income label"
  },
  "expenses": "Expenses",
  "@expenses": {
    "description": "Expenses label"
  },

  "statusLoading": "Loading...",
  "@statusLoading": {
    "description": "Loading status message"
  },
  "statusSuccess": "Success!",
  "@statusSuccess": {
    "description": "Success status message"
  },
  "statusError": "Error occurred",
  "@statusError": {
    "description": "Error status message"
  },
  "statusWarning": "Warning",
  "@statusWarning": {
    "description": "Warning status message"
  },

  "statisticsTitle": "Statistics",
  "@statisticsTitle": {
    "description": "Statistics section title"
  },
  "trendPercentage": "{sign}{percent}%",
  "@trendPercentage": {
    "description": "Trend percentage display",
    "placeholders": {
      "sign": {"type": "String"},
      "percent": {"type": "String"}
    }
  },

  "dailyGoal": "Daily Goal",
  "@dailyGoal": {
    "description": "Daily goal label"
  },
  "weeklyGoal": "Weekly Goal",
  "@weeklyGoal": {
    "description": "Weekly goal label"
  },
  "monthlyGoal": "Monthly Goal",
  "@monthlyGoal": {
    "description": "Monthly goal label"
  },

  "steps": "Steps",
  "@steps": {
    "description": "Steps metric label"
  },
  "calories": "Calories",
  "@calories": {
    "description": "Calories metric label"
  },
  "distance": "Distance",
  "@distance": {
    "description": "Distance metric label"
  },
  "activeMinutes": "Active Minutes",
  "@activeMinutes": {
    "description": "Active minutes label"
  }
}

Best Practices Summary

  1. Use locale-aware formatters: NumberFormat, percentFormat for numbers
  2. Support RTL animations: Adjust slide directions based on text direction
  3. Add tabular figures: Use FontFeature.tabularFigures() for counter animations
  4. Announce changes: Use Semantics.liveRegion for accessibility
  5. Stagger list animations: Add delays for visual appeal
  6. Cache previous values: Track changes for smooth transitions
  7. Use appropriate curves: easeOutCubic for natural deceleration
  8. Handle edge cases: Zero values, negative changes, extreme numbers
  9. Localize all text: Labels, suffixes, and formatting
  10. Test with different locales: Verify number formatting across regions

Conclusion

Proper TweenAnimationBuilder localization ensures your animated values display correctly across all languages. By using locale-aware number formatting, supporting RTL layouts, and providing accessible announcements, you create polished animations that feel native to users worldwide. The patterns shown here—animated counters, progress indicators, currency displays, and staggered lists—can be adapted to any Flutter application requiring animated localized content.

Remember to test your animations with different locales to verify that number formatting, text directions, and accessibility labels work correctly for all your supported languages.