← Back to Blog

Flutter AnimatedDefaultTextStyle Localization: Dynamic Typography, Text Transitions, and Style Inheritance

flutteranimateddefaulttextstyleanimationtypographylocalizationaccessibility

Flutter AnimatedDefaultTextStyle Localization: Dynamic Typography, Text Transitions, and Style Inheritance

AnimatedDefaultTextStyle smoothly animates changes to text styling properties that are inherited by child Text widgets. Proper localization ensures that dynamic typography, theme transitions, and style changes work seamlessly across languages with different scripts and text directions. This guide covers comprehensive strategies for localizing AnimatedDefaultTextStyle widgets in Flutter.

Understanding AnimatedDefaultTextStyle Localization

AnimatedDefaultTextStyle widgets require localization for:

  • Script-specific fonts: Different fonts for Arabic, Chinese, Devanagari scripts
  • RTL text alignment: Proper alignment for right-to-left languages
  • Dynamic font sizing: Adjusting sizes for languages with complex scripts
  • Theme transitions: Smooth styling changes between light/dark modes
  • Accessibility scaling: Respecting user font size preferences
  • State-based styling: Indicating selection, focus, and error states

Basic AnimatedDefaultTextStyle with Locale Support

Start with a simple AnimatedDefaultTextStyle that adapts to locale:

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

class LocalizedAnimatedTitle extends StatefulWidget {
  final bool isHighlighted;
  final String text;

  const LocalizedAnimatedTitle({
    super.key,
    required this.isHighlighted,
    required this.text,
  });

  @override
  State<LocalizedAnimatedTitle> createState() => _LocalizedAnimatedTitleState();
}

class _LocalizedAnimatedTitleState extends State<LocalizedAnimatedTitle> {
  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    // Determine font family based on script
    final fontFamily = _getFontFamilyForLocale(locale);

    // Adjust font size for complex scripts
    final baseFontSize = _getBaseFontSizeForLocale(locale);

    return Semantics(
      header: true,
      label: widget.isHighlighted
          ? l10n.highlightedTitleAccessibility(widget.text)
          : widget.text,
      child: AnimatedDefaultTextStyle(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        style: TextStyle(
          fontSize: widget.isHighlighted ? baseFontSize * 1.2 : baseFontSize,
          fontWeight: widget.isHighlighted ? FontWeight.bold : FontWeight.normal,
          color: widget.isHighlighted
              ? Theme.of(context).colorScheme.primary
              : Theme.of(context).colorScheme.onSurface,
          fontFamily: fontFamily,
          letterSpacing: widget.isHighlighted ? 0.5 : 0,
          height: _getLineHeightForLocale(locale),
        ),
        textAlign: isRtl ? TextAlign.right : TextAlign.left,
        child: Text(widget.text),
      ),
    );
  }

  String? _getFontFamilyForLocale(Locale locale) {
    // Return appropriate font for different scripts
    return switch (locale.languageCode) {
      'ar' || 'fa' || 'ur' => 'Noto Sans Arabic',
      'he' => 'Noto Sans Hebrew',
      'zh' => 'Noto Sans SC',
      'ja' => 'Noto Sans JP',
      'ko' => 'Noto Sans KR',
      'hi' || 'mr' => 'Noto Sans Devanagari',
      'th' => 'Noto Sans Thai',
      _ => null, // Use default
    };
  }

  double _getBaseFontSizeForLocale(Locale locale) {
    // Complex scripts may need larger base sizes
    return switch (locale.languageCode) {
      'zh' || 'ja' || 'ko' => 18,
      'ar' || 'fa' || 'he' => 17,
      'th' => 17,
      _ => 16,
    };
  }

  double _getLineHeightForLocale(Locale locale) {
    // Adjust line height for scripts with diacritics
    return switch (locale.languageCode) {
      'ar' || 'fa' || 'ur' => 1.6,
      'th' => 1.5,
      'hi' || 'mr' => 1.5,
      _ => 1.4,
    };
  }
}

ARB File Structure for AnimatedDefaultTextStyle

{
  "highlightedTitleAccessibility": "Highlighted: {title}",
  "@highlightedTitleAccessibility": {
    "description": "Accessibility label for highlighted title",
    "placeholders": {
      "title": {"type": "String"}
    }
  },
  "sectionTitle": "Features",
  "@sectionTitle": {
    "description": "Section title"
  },
  "readMore": "Read more",
  "@readMore": {
    "description": "Read more link text"
  },
  "readLess": "Read less",
  "@readLess": {
    "description": "Read less link text"
  }
}

Selectable Text with Style Animation

Create selectable text items with animated styling:

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

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

  @override
  State<LocalizedSelectableTextList> createState() =>
      _LocalizedSelectableTextListState();
}

class _LocalizedSelectableTextListState
    extends State<LocalizedSelectableTextList> {
  int _selectedIndex = 0;

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

    final options = [
      _TextOption(l10n.optionSmall, l10n.optionSmallDescription),
      _TextOption(l10n.optionMedium, l10n.optionMediumDescription),
      _TextOption(l10n.optionLarge, l10n.optionLargeDescription),
      _TextOption(l10n.optionExtraLarge, l10n.optionExtraLargeDescription),
    ];

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Padding(
          padding: const EdgeInsets.all(16),
          child: Text(
            l10n.selectSizeLabel,
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ),
        ...options.asMap().entries.map((entry) {
          final index = entry.key;
          final option = entry.value;
          final isSelected = index == _selectedIndex;

          return _LocalizedSelectableTextItem(
            title: option.title,
            description: option.description,
            isSelected: isSelected,
            onTap: () => setState(() => _selectedIndex = index),
            selectedAccessibility: l10n.optionSelected(option.title),
            unselectedAccessibility: l10n.optionUnselected(option.title),
          );
        }),
      ],
    );
  }
}

class _TextOption {
  final String title;
  final String description;

  _TextOption(this.title, this.description);
}

class _LocalizedSelectableTextItem extends StatelessWidget {
  final String title;
  final String description;
  final bool isSelected;
  final VoidCallback onTap;
  final String selectedAccessibility;
  final String unselectedAccessibility;

  const _LocalizedSelectableTextItem({
    required this.title,
    required this.description,
    required this.isSelected,
    required this.onTap,
    required this.selectedAccessibility,
    required this.unselectedAccessibility,
  });

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

    return Semantics(
      selected: isSelected,
      label: isSelected ? selectedAccessibility : unselectedAccessibility,
      child: GestureDetector(
        onTap: onTap,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 250),
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: isSelected
                ? Theme.of(context).colorScheme.primaryContainer
                : Theme.of(context).colorScheme.surface,
            borderRadius: BorderRadius.circular(12),
            border: Border.all(
              color: isSelected
                  ? Theme.of(context).colorScheme.primary
                  : Theme.of(context).colorScheme.outline.withOpacity(0.2),
              width: isSelected ? 2 : 1,
            ),
          ),
          child: Row(
            children: [
              // Radio indicator
              AnimatedContainer(
                duration: const Duration(milliseconds: 200),
                width: 24,
                height: 24,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  border: Border.all(
                    color: isSelected
                        ? Theme.of(context).colorScheme.primary
                        : Theme.of(context).colorScheme.outline,
                    width: 2,
                  ),
                ),
                child: Center(
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 200),
                    width: isSelected ? 12 : 0,
                    height: isSelected ? 12 : 0,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: Theme.of(context).colorScheme.primary,
                    ),
                  ),
                ),
              ),
              const SizedBox(width: 16),
              // Text content
              Expanded(
                child: Column(
                  crossAxisAlignment: isRtl
                      ? CrossAxisAlignment.end
                      : CrossAxisAlignment.start,
                  children: [
                    AnimatedDefaultTextStyle(
                      duration: const Duration(milliseconds: 250),
                      style: Theme.of(context).textTheme.titleMedium!.copyWith(
                        fontWeight:
                            isSelected ? FontWeight.bold : FontWeight.normal,
                        color: isSelected
                            ? Theme.of(context).colorScheme.onPrimaryContainer
                            : Theme.of(context).colorScheme.onSurface,
                      ),
                      child: Text(title),
                    ),
                    const SizedBox(height: 4),
                    AnimatedDefaultTextStyle(
                      duration: const Duration(milliseconds: 250),
                      style: Theme.of(context).textTheme.bodySmall!.copyWith(
                        color: isSelected
                            ? Theme.of(context)
                                .colorScheme
                                .onPrimaryContainer
                                .withOpacity(0.8)
                            : Theme.of(context)
                                .colorScheme
                                .onSurface
                                .withOpacity(0.6),
                      ),
                      child: Text(description),
                    ),
                  ],
                ),
              ),
              // Checkmark
              AnimatedOpacity(
                duration: const Duration(milliseconds: 200),
                opacity: isSelected ? 1 : 0,
                child: Icon(
                  Icons.check_circle,
                  color: Theme.of(context).colorScheme.primary,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Theme Transition with Text Style Animation

Create a theme toggle with animated text styles:

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

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

  @override
  State<LocalizedThemeTextDemo> createState() => _LocalizedThemeTextDemoState();
}

class _LocalizedThemeTextDemoState extends State<LocalizedThemeTextDemo> {
  bool _isDarkMode = false;

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

    // Define theme-specific styles
    final titleStyle = _isDarkMode
        ? const TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.bold,
            color: Colors.white,
            letterSpacing: 1.0,
          )
        : TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.bold,
            color: Colors.grey.shade900,
            letterSpacing: 0.5,
          );

    final bodyStyle = _isDarkMode
        ? TextStyle(
            fontSize: 16,
            color: Colors.grey.shade300,
            height: 1.6,
          )
        : TextStyle(
            fontSize: 16,
            color: Colors.grey.shade700,
            height: 1.5,
          );

    final captionStyle = _isDarkMode
        ? TextStyle(
            fontSize: 12,
            color: Colors.grey.shade500,
            fontStyle: FontStyle.italic,
          )
        : TextStyle(
            fontSize: 12,
            color: Colors.grey.shade600,
            fontStyle: FontStyle.normal,
          );

    return AnimatedContainer(
      duration: const Duration(milliseconds: 400),
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: _isDarkMode ? const Color(0xFF1A1A2E) : Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment:
            isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: [
          // Theme toggle
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              AnimatedDefaultTextStyle(
                duration: const Duration(milliseconds: 300),
                style: bodyStyle,
                child: Text(l10n.themeLabel),
              ),
              Semantics(
                label: _isDarkMode
                    ? l10n.switchToLightMode
                    : l10n.switchToDarkMode,
                child: Switch(
                  value: _isDarkMode,
                  onChanged: (value) => setState(() => _isDarkMode = value),
                ),
              ),
            ],
          ),
          const SizedBox(height: 24),
          // Animated title
          AnimatedDefaultTextStyle(
            duration: const Duration(milliseconds: 400),
            style: titleStyle,
            child: Text(l10n.sampleTitle),
          ),
          const SizedBox(height: 16),
          // Animated body
          AnimatedDefaultTextStyle(
            duration: const Duration(milliseconds: 400),
            style: bodyStyle,
            child: Text(l10n.sampleBody),
          ),
          const SizedBox(height: 12),
          // Animated caption
          AnimatedDefaultTextStyle(
            duration: const Duration(milliseconds: 400),
            style: captionStyle,
            child: Text(l10n.sampleCaption),
          ),
        ],
      ),
    );
  }
}

Text Size Accessibility Controls

Create text size controls with animated transitions:

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

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

  @override
  State<LocalizedTextSizeControls> createState() =>
      _LocalizedTextSizeControlsState();
}

class _LocalizedTextSizeControlsState extends State<LocalizedTextSizeControls> {
  double _textScale = 1.0;
  static const double _minScale = 0.8;
  static const double _maxScale = 1.5;

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

    return Column(
      crossAxisAlignment:
          isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start,
      children: [
        // Size controls
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.surfaceVariant,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    l10n.textSizeLabel,
                    style: Theme.of(context).textTheme.titleSmall,
                  ),
                  Text(
                    l10n.textSizePercent((_textScale * 100).round()),
                    style: Theme.of(context).textTheme.labelLarge?.copyWith(
                      color: Theme.of(context).colorScheme.primary,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  IconButton(
                    onPressed: _textScale > _minScale
                        ? () => setState(() {
                              _textScale = (_textScale - 0.1).clamp(
                                _minScale,
                                _maxScale,
                              );
                            })
                        : null,
                    icon: const Icon(Icons.text_decrease),
                    tooltip: l10n.decreaseTextSize,
                  ),
                  Expanded(
                    child: Semantics(
                      label: l10n.textSizeSliderAccessibility(
                        (_textScale * 100).round(),
                      ),
                      child: Slider(
                        value: _textScale,
                        min: _minScale,
                        max: _maxScale,
                        divisions: 7,
                        onChanged: (value) => setState(() => _textScale = value),
                      ),
                    ),
                  ),
                  IconButton(
                    onPressed: _textScale < _maxScale
                        ? () => setState(() {
                              _textScale = (_textScale + 0.1).clamp(
                                _minScale,
                                _maxScale,
                              );
                            })
                        : null,
                    icon: const Icon(Icons.text_increase),
                    tooltip: l10n.increaseTextSize,
                  ),
                ],
              ),
              // Quick select buttons
              const SizedBox(height: 8),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  _QuickScaleButton(
                    label: l10n.sizeSmall,
                    scale: 0.85,
                    currentScale: _textScale,
                    onTap: () => setState(() => _textScale = 0.85),
                  ),
                  _QuickScaleButton(
                    label: l10n.sizeNormal,
                    scale: 1.0,
                    currentScale: _textScale,
                    onTap: () => setState(() => _textScale = 1.0),
                  ),
                  _QuickScaleButton(
                    label: l10n.sizeLarge,
                    scale: 1.2,
                    currentScale: _textScale,
                    onTap: () => setState(() => _textScale = 1.2),
                  ),
                  _QuickScaleButton(
                    label: l10n.sizeExtraLarge,
                    scale: 1.4,
                    currentScale: _textScale,
                    onTap: () => setState(() => _textScale = 1.4),
                  ),
                ],
              ),
            ],
          ),
        ),
        const SizedBox(height: 24),
        // Preview text
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            border: Border.all(
              color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
            ),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Column(
            crossAxisAlignment:
                isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start,
            children: [
              Text(
                l10n.previewLabel,
                style: Theme.of(context).textTheme.labelMedium?.copyWith(
                  color: Theme.of(context).colorScheme.outline,
                ),
              ),
              const SizedBox(height: 12),
              AnimatedDefaultTextStyle(
                duration: const Duration(milliseconds: 200),
                style: baseStyle.copyWith(
                  fontSize: baseStyle.fontSize! * _textScale,
                  height: 1.5,
                ),
                child: Text(l10n.previewText),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _QuickScaleButton extends StatelessWidget {
  final String label;
  final double scale;
  final double currentScale;
  final VoidCallback onTap;

  const _QuickScaleButton({
    required this.label,
    required this.scale,
    required this.currentScale,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final isSelected = (currentScale - scale).abs() < 0.05;

    return GestureDetector(
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: isSelected
              ? Theme.of(context).colorScheme.primary
              : Colors.transparent,
          borderRadius: BorderRadius.circular(16),
          border: Border.all(
            color: isSelected
                ? Theme.of(context).colorScheme.primary
                : Theme.of(context).colorScheme.outline.withOpacity(0.5),
          ),
        ),
        child: AnimatedDefaultTextStyle(
          duration: const Duration(milliseconds: 200),
          style: Theme.of(context).textTheme.labelMedium!.copyWith(
            color: isSelected
                ? Theme.of(context).colorScheme.onPrimary
                : Theme.of(context).colorScheme.onSurface,
            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          ),
          child: Text(label),
        ),
      ),
    );
  }
}

Form Validation with Animated Error Styles

Create form fields with animated error text styles:

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

class LocalizedAnimatedFormField extends StatefulWidget {
  final String label;
  final String hint;
  final String? Function(String?) validator;
  final TextEditingController? controller;
  final bool obscureText;

  const LocalizedAnimatedFormField({
    super.key,
    required this.label,
    required this.hint,
    required this.validator,
    this.controller,
    this.obscureText = false,
  });

  @override
  State<LocalizedAnimatedFormField> createState() =>
      _LocalizedAnimatedFormFieldState();
}

class _LocalizedAnimatedFormFieldState
    extends State<LocalizedAnimatedFormField> {
  String? _errorMessage;
  bool _isFocused = false;
  bool _hasInteracted = false;
  final FocusNode _focusNode = FocusNode();
  late TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = widget.controller ?? TextEditingController();
    _focusNode.addListener(_handleFocusChange);
    _controller.addListener(_validateOnChange);
  }

  void _handleFocusChange() {
    setState(() {
      _isFocused = _focusNode.hasFocus;
      if (!_focusNode.hasFocus && _hasInteracted) {
        _validate();
      }
    });
  }

  void _validateOnChange() {
    if (_hasInteracted) {
      _validate();
    }
  }

  void _validate() {
    setState(() {
      _hasInteracted = true;
      _errorMessage = widget.validator(_controller.text);
    });
  }

  @override
  void dispose() {
    _focusNode.removeListener(_handleFocusChange);
    _focusNode.dispose();
    if (widget.controller == null) {
      _controller.dispose();
    }
    super.dispose();
  }

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

    return Column(
      crossAxisAlignment:
          isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start,
      children: [
        // Label
        AnimatedDefaultTextStyle(
          duration: const Duration(milliseconds: 200),
          style: Theme.of(context).textTheme.labelLarge!.copyWith(
            color: hasError
                ? Theme.of(context).colorScheme.error
                : _isFocused
                    ? Theme.of(context).colorScheme.primary
                    : Theme.of(context).colorScheme.onSurface,
            fontWeight: _isFocused ? FontWeight.bold : FontWeight.normal,
          ),
          child: Text(widget.label),
        ),
        const SizedBox(height: 8),
        // Input field
        AnimatedContainer(
          duration: const Duration(milliseconds: 200),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(_isFocused ? 12 : 8),
            border: Border.all(
              color: hasError
                  ? Theme.of(context).colorScheme.error
                  : _isFocused
                      ? Theme.of(context).colorScheme.primary
                      : Theme.of(context).colorScheme.outline.withOpacity(0.3),
              width: _isFocused || hasError ? 2 : 1,
            ),
            boxShadow: _isFocused
                ? [
                    BoxShadow(
                      color: (hasError
                              ? Theme.of(context).colorScheme.error
                              : Theme.of(context).colorScheme.primary)
                          .withOpacity(0.1),
                      blurRadius: 8,
                      offset: const Offset(0, 2),
                    ),
                  ]
                : null,
          ),
          child: TextField(
            controller: _controller,
            focusNode: _focusNode,
            obscureText: widget.obscureText,
            textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
            decoration: InputDecoration(
              hintText: widget.hint,
              border: InputBorder.none,
              contentPadding: const EdgeInsets.symmetric(
                horizontal: 16,
                vertical: 12,
              ),
              suffixIcon: hasError
                  ? Icon(
                      Icons.error_outline,
                      color: Theme.of(context).colorScheme.error,
                    )
                  : null,
            ),
            onTap: () => setState(() => _hasInteracted = true),
          ),
        ),
        // Error message
        AnimatedSize(
          duration: const Duration(milliseconds: 200),
          child: AnimatedOpacity(
            duration: const Duration(milliseconds: 200),
            opacity: hasError ? 1 : 0,
            child: hasError
                ? Padding(
                    padding: const EdgeInsets.only(top: 8),
                    child: Row(
                      children: [
                        Icon(
                          Icons.info_outline,
                          size: 16,
                          color: Theme.of(context).colorScheme.error,
                        ),
                        const SizedBox(width: 4),
                        Expanded(
                          child: AnimatedDefaultTextStyle(
                            duration: const Duration(milliseconds: 200),
                            style:
                                Theme.of(context).textTheme.bodySmall!.copyWith(
                              color: Theme.of(context).colorScheme.error,
                            ),
                            child: Text(_errorMessage ?? ''),
                          ),
                        ),
                      ],
                    ),
                  )
                : const SizedBox.shrink(),
          ),
        ),
      ],
    );
  }
}

Tab Labels with Animated Text Style

Create animated tab labels:

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

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

  @override
  State<LocalizedAnimatedTabs> createState() => _LocalizedAnimatedTabsState();
}

class _LocalizedAnimatedTabsState extends State<LocalizedAnimatedTabs> {
  int _selectedIndex = 0;

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

    final tabs = [
      _TabData(l10n.tabOverview, Icons.dashboard),
      _TabData(l10n.tabDetails, Icons.list_alt),
      _TabData(l10n.tabReviews, Icons.rate_review),
      _TabData(l10n.tabRelated, Icons.apps),
    ];

    return Column(
      children: [
        // Tab bar
        Container(
          height: 56,
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.surfaceVariant,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Row(
            textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
            children: tabs.asMap().entries.map((entry) {
              final index = entry.key;
              final tab = entry.value;
              final isSelected = index == _selectedIndex;

              return Expanded(
                child: Semantics(
                  selected: isSelected,
                  label: l10n.tabAccessibilityLabel(
                    tab.label,
                    index + 1,
                    tabs.length,
                  ),
                  child: GestureDetector(
                    onTap: () => setState(() => _selectedIndex = index),
                    behavior: HitTestBehavior.opaque,
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 200),
                      margin: const EdgeInsets.all(4),
                      decoration: BoxDecoration(
                        color: isSelected
                            ? Theme.of(context).colorScheme.surface
                            : Colors.transparent,
                        borderRadius: BorderRadius.circular(8),
                        boxShadow: isSelected
                            ? [
                                BoxShadow(
                                  color: Colors.black.withOpacity(0.05),
                                  blurRadius: 4,
                                  offset: const Offset(0, 2),
                                ),
                              ]
                            : null,
                      ),
                      child: Center(
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            AnimatedContainer(
                              duration: const Duration(milliseconds: 200),
                              child: Icon(
                                tab.icon,
                                size: isSelected ? 20 : 18,
                                color: isSelected
                                    ? Theme.of(context).colorScheme.primary
                                    : Theme.of(context)
                                        .colorScheme
                                        .onSurfaceVariant,
                              ),
                            ),
                            const SizedBox(width: 6),
                            AnimatedDefaultTextStyle(
                              duration: const Duration(milliseconds: 200),
                              style: Theme.of(context)
                                  .textTheme
                                  .labelMedium!
                                  .copyWith(
                                fontSize: isSelected ? 14 : 13,
                                fontWeight: isSelected
                                    ? FontWeight.bold
                                    : FontWeight.normal,
                                color: isSelected
                                    ? Theme.of(context).colorScheme.primary
                                    : Theme.of(context)
                                        .colorScheme
                                        .onSurfaceVariant,
                              ),
                              child: Text(tab.label),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ),
                ),
              );
            }).toList(),
          ),
        ),
        const SizedBox(height: 16),
        // Tab content
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 200),
          child: Container(
            key: ValueKey(_selectedIndex),
            padding: const EdgeInsets.all(24),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surface,
              borderRadius: BorderRadius.circular(12),
              border: Border.all(
                color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
              ),
            ),
            child: Center(
              child: Text(
                l10n.tabContentPlaceholder(tabs[_selectedIndex].label),
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class _TabData {
  final String label;
  final IconData icon;

  _TabData(this.label, this.icon);
}

Complete ARB File for AnimatedDefaultTextStyle

{
  "@@locale": "en",

  "highlightedTitleAccessibility": "Highlighted: {title}",
  "@highlightedTitleAccessibility": {
    "placeholders": {
      "title": {"type": "String"}
    }
  },

  "selectSizeLabel": "Select a size option:",
  "optionSmall": "Small",
  "optionSmallDescription": "Compact size for minimal footprint",
  "optionMedium": "Medium",
  "optionMediumDescription": "Balanced size for everyday use",
  "optionLarge": "Large",
  "optionLargeDescription": "Generous size for comfort",
  "optionExtraLarge": "Extra Large",
  "optionExtraLargeDescription": "Maximum size for best visibility",
  "optionSelected": "{option} selected",
  "@optionSelected": {
    "placeholders": {
      "option": {"type": "String"}
    }
  },
  "optionUnselected": "{option}, tap to select",
  "@optionUnselected": {
    "placeholders": {
      "option": {"type": "String"}
    }
  },

  "themeLabel": "Dark mode",
  "switchToLightMode": "Switch to light mode",
  "switchToDarkMode": "Switch to dark mode",
  "sampleTitle": "Welcome to Our App",
  "sampleBody": "This is a sample paragraph demonstrating how text styles animate smoothly between different themes. Notice how the colors, weights, and spacing transition gracefully.",
  "sampleCaption": "Last updated: Today",

  "textSizeLabel": "Text Size",
  "textSizePercent": "{percent}%",
  "@textSizePercent": {
    "placeholders": {
      "percent": {"type": "int"}
    }
  },
  "decreaseTextSize": "Decrease text size",
  "increaseTextSize": "Increase text size",
  "textSizeSliderAccessibility": "Text size at {percent} percent",
  "@textSizeSliderAccessibility": {
    "placeholders": {
      "percent": {"type": "int"}
    }
  },
  "sizeSmall": "Small",
  "sizeNormal": "Normal",
  "sizeLarge": "Large",
  "sizeExtraLarge": "XL",
  "previewLabel": "Preview",
  "previewText": "The quick brown fox jumps over the lazy dog. This sentence contains every letter of the alphabet and is perfect for previewing text styles.",

  "tabOverview": "Overview",
  "tabDetails": "Details",
  "tabReviews": "Reviews",
  "tabRelated": "Related",
  "tabAccessibilityLabel": "{label}, tab {current} of {total}",
  "@tabAccessibilityLabel": {
    "placeholders": {
      "label": {"type": "String"},
      "current": {"type": "int"},
      "total": {"type": "int"}
    }
  },
  "tabContentPlaceholder": "{tab} content goes here",
  "@tabContentPlaceholder": {
    "placeholders": {
      "tab": {"type": "String"}
    }
  }
}

Best Practices Summary

  1. Use locale-appropriate fonts: Load fonts that support the target script (Arabic, CJK, Devanagari)
  2. Adjust line heights: Scripts with diacritics need more vertical space
  3. Support text scaling: Always test with system text size multipliers
  4. Animate purposefully: Use text style changes to indicate state transitions
  5. Consider script complexity: Some scripts need larger base font sizes
  6. Handle RTL alignment: Use TextAlign.start/end instead of left/right
  7. Provide accessibility labels: Announce significant style changes
  8. Test with real translations: Some languages have much longer text
  9. Use semantic colors: Error, warning, success states should use appropriate colors
  10. Respect system settings: Honor platform accessibility preferences

Conclusion

Proper AnimatedDefaultTextStyle localization ensures smooth, accessible typography transitions across all languages and scripts. By handling locale-specific fonts, adjusting for text expansion, and providing comprehensive accessibility support, you create polished text animations that feel native to users worldwide. The patterns shown here—selectable lists, theme transitions, text size controls, and form validation—can be adapted to any Flutter application requiring animated text styling.

Remember to test your text style animations with different locales to verify that fonts, sizing, and accessibility work correctly for all your supported languages and scripts.