← Back to Blog

Flutter AnimatedScale Localization: Zoom Effects, Interactive Feedback, and Accessible Scaling

flutteranimatedscaleanimationzoomlocalizationaccessibility

Flutter AnimatedScale Localization: Zoom Effects, Interactive Feedback, and Accessible Scaling

AnimatedScale provides smooth scaling animations for widgets. Proper localization ensures that zoom effects, press feedback, and emphasis animations work seamlessly across languages with appropriate accessibility announcements. This guide covers comprehensive strategies for localizing AnimatedScale widgets in Flutter.

Understanding AnimatedScale Localization

AnimatedScale widgets require localization for:

  • Button press feedback: Scaling effects with localized labels
  • Selection indicators: Emphasis animations for selected items
  • Card highlights: Hover and focus states for interactive elements
  • Image zoom: Preview scaling with accessible descriptions
  • Attention animations: Drawing focus to important localized content
  • Accessibility feedback: Screen reader announcements for scale changes

Basic AnimatedScale with Interactive Feedback

Start with a simple button that scales on press:

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

class LocalizedScaleButton extends StatefulWidget {
  final VoidCallback onPressed;
  final String label;

  const LocalizedScaleButton({
    super.key,
    required this.onPressed,
    required this.label,
  });

  @override
  State<LocalizedScaleButton> createState() => _LocalizedScaleButtonState();
}

class _LocalizedScaleButtonState extends State<LocalizedScaleButton> {
  bool _isPressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _isPressed = true),
      onTapUp: (_) {
        setState(() => _isPressed = false);
        widget.onPressed();
      },
      onTapCancel: () => setState(() => _isPressed = false),
      child: Semantics(
        button: true,
        label: widget.label,
        child: AnimatedScale(
          scale: _isPressed ? 0.95 : 1.0,
          duration: const Duration(milliseconds: 100),
          curve: Curves.easeInOut,
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.primary,
              borderRadius: BorderRadius.circular(8),
              boxShadow: _isPressed
                  ? []
                  : [
                      BoxShadow(
                        color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
                        blurRadius: 8,
                        offset: const Offset(0, 4),
                      ),
                    ],
            ),
            child: Text(
              widget.label,
              style: TextStyle(
                color: Theme.of(context).colorScheme.onPrimary,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.buttonDemoTitle)),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            LocalizedScaleButton(
              label: l10n.submitButton,
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(l10n.formSubmitted)),
                );
              },
            ),
            const SizedBox(height: 16),
            LocalizedScaleButton(
              label: l10n.cancelButton,
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        ),
      ),
    );
  }
}

ARB File Structure for AnimatedScale

{
  "buttonDemoTitle": "Button Demo",
  "@buttonDemoTitle": {
    "description": "Title for button demo screen"
  },
  "submitButton": "Submit",
  "@submitButton": {
    "description": "Label for submit button"
  },
  "cancelButton": "Cancel",
  "@cancelButton": {
    "description": "Label for cancel button"
  },
  "formSubmitted": "Form submitted successfully",
  "@formSubmitted": {
    "description": "Success message after form submission"
  }
}

Selectable Card Grid with Scale Animation

Create a grid of selectable cards with scaling effects:

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

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

  @override
  State<LocalizedSelectableCardGrid> createState() => _LocalizedSelectableCardGridState();
}

class _LocalizedSelectableCardGridState extends State<LocalizedSelectableCardGrid> {
  final Set<int> _selectedIndices = {};

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

    final categories = [
      _Category(l10n.categoryTechnology, Icons.computer, Colors.blue),
      _Category(l10n.categoryTravel, Icons.flight, Colors.green),
      _Category(l10n.categoryFood, Icons.restaurant, Colors.orange),
      _Category(l10n.categorySports, Icons.sports_soccer, Colors.red),
      _Category(l10n.categoryMusic, Icons.music_note, Colors.purple),
      _Category(l10n.categoryArt, Icons.palette, Colors.pink),
    ];

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.selectInterestsTitle),
        actions: [
          if (_selectedIndices.isNotEmpty)
            TextButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text(l10n.interestsSelected(_selectedIndices.length)),
                  ),
                );
              },
              child: Text(l10n.doneButton),
            ),
        ],
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(
              l10n.selectInterestsDescription,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ),
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(16),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 16,
                mainAxisSpacing: 16,
                childAspectRatio: 1.2,
              ),
              itemCount: categories.length,
              itemBuilder: (context, index) {
                final category = categories[index];
                final isSelected = _selectedIndices.contains(index);

                return _SelectableCard(
                  category: category,
                  isSelected: isSelected,
                  accessibilityLabel: l10n.categoryAccessibility(
                    category.name,
                    isSelected ? l10n.selected : l10n.notSelected,
                  ),
                  onTap: () {
                    setState(() {
                      if (isSelected) {
                        _selectedIndices.remove(index);
                      } else {
                        _selectedIndices.add(index);
                      }
                    });
                  },
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(
              l10n.selectedCount(_selectedIndices.length),
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Theme.of(context).colorScheme.outline,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _Category {
  final String name;
  final IconData icon;
  final Color color;

  _Category(this.name, this.icon, this.color);
}

class _SelectableCard extends StatefulWidget {
  final _Category category;
  final bool isSelected;
  final String accessibilityLabel;
  final VoidCallback onTap;

  const _SelectableCard({
    required this.category,
    required this.isSelected,
    required this.accessibilityLabel,
    required this.onTap,
  });

  @override
  State<_SelectableCard> createState() => _SelectableCardState();
}

class _SelectableCardState extends State<_SelectableCard> {
  bool _isPressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _isPressed = true),
      onTapUp: (_) {
        setState(() => _isPressed = false);
        widget.onTap();
      },
      onTapCancel: () => setState(() => _isPressed = false),
      child: Semantics(
        button: true,
        selected: widget.isSelected,
        label: widget.accessibilityLabel,
        child: AnimatedScale(
          scale: _isPressed
              ? 0.95
              : widget.isSelected
                  ? 1.05
                  : 1.0,
          duration: const Duration(milliseconds: 150),
          curve: Curves.easeOut,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 200),
            decoration: BoxDecoration(
              color: widget.isSelected
                  ? widget.category.color.withOpacity(0.2)
                  : Theme.of(context).colorScheme.surface,
              borderRadius: BorderRadius.circular(16),
              border: Border.all(
                color: widget.isSelected
                    ? widget.category.color
                    : Theme.of(context).colorScheme.outline.withOpacity(0.3),
                width: widget.isSelected ? 2 : 1,
              ),
              boxShadow: widget.isSelected
                  ? [
                      BoxShadow(
                        color: widget.category.color.withOpacity(0.3),
                        blurRadius: 12,
                        offset: const Offset(0, 4),
                      ),
                    ]
                  : [],
            ),
            child: Stack(
              children: [
                Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Icon(
                        widget.category.icon,
                        size: 40,
                        color: widget.isSelected
                            ? widget.category.color
                            : Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                      const SizedBox(height: 8),
                      Text(
                        widget.category.name,
                        style: Theme.of(context).textTheme.titleMedium?.copyWith(
                          color: widget.isSelected
                              ? widget.category.color
                              : Theme.of(context).colorScheme.onSurface,
                          fontWeight: widget.isSelected
                              ? FontWeight.bold
                              : FontWeight.normal,
                        ),
                      ),
                    ],
                  ),
                ),
                if (widget.isSelected)
                  Positioned(
                    top: 8,
                    right: 8,
                    child: Icon(
                      Icons.check_circle,
                      color: widget.category.color,
                    ),
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Image Preview with Zoom Animation

Create an image preview with scale-based zoom:

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

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

  @override
  State<LocalizedImagePreview> createState() => _LocalizedImagePreviewState();
}

class _LocalizedImagePreviewState extends State<LocalizedImagePreview> {
  bool _isZoomed = false;
  int _selectedIndex = 0;

  final List<String> _imageUrls = [
    'https://picsum.photos/400/300?random=1',
    'https://picsum.photos/400/300?random=2',
    'https://picsum.photos/400/300?random=3',
  ];

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.imageGalleryTitle),
        actions: [
          IconButton(
            icon: Icon(_isZoomed ? Icons.zoom_out : Icons.zoom_in),
            tooltip: _isZoomed ? l10n.zoomOutTooltip : l10n.zoomInTooltip,
            onPressed: () => setState(() => _isZoomed = !_isZoomed),
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: GestureDetector(
              onDoubleTap: () => setState(() => _isZoomed = !_isZoomed),
              child: Semantics(
                image: true,
                label: l10n.imageAccessibility(
                  _selectedIndex + 1,
                  _imageUrls.length,
                  _isZoomed ? l10n.zoomed : l10n.normal,
                ),
                child: Center(
                  child: AnimatedScale(
                    scale: _isZoomed ? 1.5 : 1.0,
                    duration: const Duration(milliseconds: 300),
                    curve: Curves.easeOutBack,
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(16),
                      child: Image.network(
                        _imageUrls[_selectedIndex],
                        fit: BoxFit.cover,
                        width: MediaQuery.of(context).size.width * 0.8,
                        loadingBuilder: (context, child, loadingProgress) {
                          if (loadingProgress == null) return child;
                          return Container(
                            width: MediaQuery.of(context).size.width * 0.8,
                            height: 200,
                            color: Theme.of(context).colorScheme.surfaceVariant,
                            child: Center(
                              child: Column(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  const CircularProgressIndicator(),
                                  const SizedBox(height: 8),
                                  Text(l10n.loadingImage),
                                ],
                              ),
                            ),
                          );
                        },
                        errorBuilder: (context, error, stackTrace) {
                          return Container(
                            width: MediaQuery.of(context).size.width * 0.8,
                            height: 200,
                            color: Theme.of(context).colorScheme.errorContainer,
                            child: Center(
                              child: Column(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  Icon(
                                    Icons.error,
                                    color: Theme.of(context).colorScheme.error,
                                  ),
                                  const SizedBox(height: 8),
                                  Text(l10n.imageLoadError),
                                ],
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(
              l10n.doubleTapToZoom,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: Theme.of(context).colorScheme.outline,
              ),
            ),
          ),
          // Thumbnail strip
          SizedBox(
            height: 80,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 16),
              itemCount: _imageUrls.length,
              itemBuilder: (context, index) {
                final isSelected = index == _selectedIndex;
                return Padding(
                  padding: const EdgeInsets.only(right: 8),
                  child: GestureDetector(
                    onTap: () => setState(() {
                      _selectedIndex = index;
                      _isZoomed = false;
                    }),
                    child: Semantics(
                      button: true,
                      selected: isSelected,
                      label: l10n.selectImage(index + 1),
                      child: AnimatedScale(
                        scale: isSelected ? 1.1 : 1.0,
                        duration: const Duration(milliseconds: 200),
                        child: AnimatedContainer(
                          duration: const Duration(milliseconds: 200),
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(8),
                            border: Border.all(
                              color: isSelected
                                  ? Theme.of(context).colorScheme.primary
                                  : Colors.transparent,
                              width: 3,
                            ),
                          ),
                          child: ClipRRect(
                            borderRadius: BorderRadius.circular(6),
                            child: Image.network(
                              _imageUrls[index],
                              width: 60,
                              height: 60,
                              fit: BoxFit.cover,
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
          const SizedBox(height: 16),
        ],
      ),
    );
  }
}

Notification Badge with Pulse Animation

Create a notification badge with attention-grabbing scale animation:

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

class LocalizedNotificationBadge extends StatefulWidget {
  final int count;
  final Widget child;
  final String itemLabel;

  const LocalizedNotificationBadge({
    super.key,
    required this.count,
    required this.child,
    required this.itemLabel,
  });

  @override
  State<LocalizedNotificationBadge> createState() => _LocalizedNotificationBadgeState();
}

class _LocalizedNotificationBadgeState extends State<LocalizedNotificationBadge>
    with SingleTickerProviderStateMixin {
  late AnimationController _pulseController;
  int _previousCount = 0;

  @override
  void initState() {
    super.initState();
    _previousCount = widget.count;
    _pulseController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void didUpdateWidget(LocalizedNotificationBadge oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.count > _previousCount) {
      _pulseController.forward().then((_) => _pulseController.reverse());
    }
    _previousCount = widget.count;
  }

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

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

    return Semantics(
      label: widget.count > 0
          ? l10n.notificationBadgeAccessibility(widget.itemLabel, widget.count)
          : widget.itemLabel,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          widget.child,
          if (widget.count > 0)
            Positioned(
              top: -4,
              right: -4,
              child: AnimatedBuilder(
                animation: _pulseController,
                builder: (context, child) {
                  return AnimatedScale(
                    scale: 1.0 + (_pulseController.value * 0.3),
                    duration: Duration.zero,
                    child: child,
                  );
                },
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.error,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  constraints: const BoxConstraints(minWidth: 20),
                  child: Text(
                    widget.count > 99 ? '99+' : widget.count.toString(),
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.onError,
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

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

  @override
  State<NotificationBadgeDemo> createState() => _NotificationBadgeDemoState();
}

class _NotificationBadgeDemoState extends State<NotificationBadgeDemo> {
  int _messageCount = 3;
  int _notificationCount = 0;
  int _cartCount = 1;

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.notificationDemoTitle),
        actions: [
          LocalizedNotificationBadge(
            count: _notificationCount,
            itemLabel: l10n.notificationsLabel,
            child: IconButton(
              icon: const Icon(Icons.notifications),
              onPressed: () => setState(() => _notificationCount = 0),
              tooltip: l10n.notificationsLabel,
            ),
          ),
          LocalizedNotificationBadge(
            count: _cartCount,
            itemLabel: l10n.cartLabel,
            child: IconButton(
              icon: const Icon(Icons.shopping_cart),
              onPressed: () {},
              tooltip: l10n.cartLabel,
            ),
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            LocalizedNotificationBadge(
              count: _messageCount,
              itemLabel: l10n.messagesLabel,
              child: FloatingActionButton(
                heroTag: 'messages',
                onPressed: () {},
                child: const Icon(Icons.message),
              ),
            ),
            const SizedBox(height: 32),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => setState(() => _messageCount++),
                  child: Text(l10n.addMessage),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: () => setState(() => _notificationCount++),
                  child: Text(l10n.addNotification),
                ),
              ],
            ),
            const SizedBox(height: 16),
            TextButton(
              onPressed: () => setState(() {
                _messageCount = 0;
                _notificationCount = 0;
                _cartCount = 0;
              }),
              child: Text(l10n.clearAll),
            ),
          ],
        ),
      ),
    );
  }
}

Floating Action Button with Scale Effect

Create a FAB menu with scale animations:

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

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

  @override
  State<LocalizedScalableFAB> createState() => _LocalizedScalableFABState();
}

class _LocalizedScalableFABState extends State<LocalizedScalableFAB> {
  bool _isExpanded = false;

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

    final actions = [
      _FABAction(Icons.camera_alt, l10n.takePhoto, Colors.blue),
      _FABAction(Icons.photo_library, l10n.chooseFromGallery, Colors.green),
      _FABAction(Icons.insert_drive_file, l10n.attachFile, Colors.orange),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.fabMenuTitle)),
      body: Center(
        child: Text(l10n.fabMenuDescription),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: isRtl ? CrossAxisAlignment.start : CrossAxisAlignment.end,
        children: [
          ...actions.asMap().entries.map((entry) {
            final index = entry.key;
            final action = entry.value;
            final delay = (actions.length - 1 - index) * 50;

            return Padding(
              padding: const EdgeInsets.only(bottom: 16),
              child: AnimatedScale(
                scale: _isExpanded ? 1.0 : 0.0,
                duration: Duration(milliseconds: 200 + delay),
                curve: Curves.easeOutBack,
                child: AnimatedOpacity(
                  opacity: _isExpanded ? 1.0 : 0.0,
                  duration: Duration(milliseconds: 150 + delay),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      if (!isRtl)
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 12,
                            vertical: 6,
                          ),
                          decoration: BoxDecoration(
                            color: Theme.of(context).colorScheme.surface,
                            borderRadius: BorderRadius.circular(4),
                            boxShadow: [
                              BoxShadow(
                                color: Colors.black.withOpacity(0.1),
                                blurRadius: 4,
                              ),
                            ],
                          ),
                          child: Text(action.label),
                        ),
                      if (!isRtl) const SizedBox(width: 8),
                      FloatingActionButton.small(
                        heroTag: 'fab_$index',
                        backgroundColor: action.color,
                        onPressed: () {
                          setState(() => _isExpanded = false);
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text(action.label)),
                          );
                        },
                        tooltip: action.label,
                        child: Icon(action.icon, color: Colors.white),
                      ),
                      if (isRtl) const SizedBox(width: 8),
                      if (isRtl)
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 12,
                            vertical: 6,
                          ),
                          decoration: BoxDecoration(
                            color: Theme.of(context).colorScheme.surface,
                            borderRadius: BorderRadius.circular(4),
                            boxShadow: [
                              BoxShadow(
                                color: Colors.black.withOpacity(0.1),
                                blurRadius: 4,
                              ),
                            ],
                          ),
                          child: Text(action.label),
                        ),
                    ],
                  ),
                ),
              ),
            );
          }),
          FloatingActionButton(
            onPressed: () => setState(() => _isExpanded = !_isExpanded),
            tooltip: _isExpanded ? l10n.closeMenu : l10n.openMenu,
            child: AnimatedRotation(
              turns: _isExpanded ? 0.125 : 0,
              duration: const Duration(milliseconds: 200),
              child: const Icon(Icons.add),
            ),
          ),
        ],
      ),
    );
  }
}

class _FABAction {
  final IconData icon;
  final String label;
  final Color color;

  _FABAction(this.icon, this.label, this.color);
}

Complete ARB File for AnimatedScale

{
  "@@locale": "en",

  "buttonDemoTitle": "Button Demo",
  "submitButton": "Submit",
  "cancelButton": "Cancel",
  "formSubmitted": "Form submitted successfully",

  "selectInterestsTitle": "Select Your Interests",
  "selectInterestsDescription": "Choose the categories that interest you most. Tap to select or deselect.",
  "categoryTechnology": "Technology",
  "categoryTravel": "Travel",
  "categoryFood": "Food",
  "categorySports": "Sports",
  "categoryMusic": "Music",
  "categoryArt": "Art",
  "selected": "selected",
  "notSelected": "not selected",
  "categoryAccessibility": "{name}, {state}",
  "@categoryAccessibility": {
    "placeholders": {
      "name": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "selectedCount": "{count} selected",
  "@selectedCount": {
    "placeholders": {
      "count": {"type": "int"}
    }
  },
  "doneButton": "Done",
  "interestsSelected": "Saved {count} interests",
  "@interestsSelected": {
    "placeholders": {
      "count": {"type": "int"}
    }
  },

  "imageGalleryTitle": "Image Gallery",
  "zoomInTooltip": "Zoom in",
  "zoomOutTooltip": "Zoom out",
  "zoomed": "zoomed in",
  "normal": "normal view",
  "imageAccessibility": "Image {current} of {total}, {state}",
  "@imageAccessibility": {
    "placeholders": {
      "current": {"type": "int"},
      "total": {"type": "int"},
      "state": {"type": "String"}
    }
  },
  "loadingImage": "Loading image...",
  "imageLoadError": "Failed to load image",
  "doubleTapToZoom": "Double-tap to zoom",
  "selectImage": "Select image {number}",
  "@selectImage": {
    "placeholders": {
      "number": {"type": "int"}
    }
  },

  "notificationDemoTitle": "Notifications",
  "notificationsLabel": "Notifications",
  "cartLabel": "Shopping cart",
  "messagesLabel": "Messages",
  "notificationBadgeAccessibility": "{item} with {count} new items",
  "@notificationBadgeAccessibility": {
    "placeholders": {
      "item": {"type": "String"},
      "count": {"type": "int"}
    }
  },
  "addMessage": "Add Message",
  "addNotification": "Add Notification",
  "clearAll": "Clear All",

  "fabMenuTitle": "FAB Menu",
  "fabMenuDescription": "Tap the + button to see actions",
  "takePhoto": "Take Photo",
  "chooseFromGallery": "Choose from Gallery",
  "attachFile": "Attach File",
  "openMenu": "Open menu",
  "closeMenu": "Close menu"
}

Best Practices Summary

  1. Use subtle scale values: 0.95-1.05 for interactive feedback, larger for emphasis
  2. Provide press feedback: Scale down on tap to indicate interaction
  3. Handle accessibility: Announce scale changes for meaningful state transitions
  4. Combine with other effects: Mix scale with opacity and shadow changes
  5. Choose appropriate durations: 100-150ms for press feedback, 200-300ms for emphasis
  6. Test RTL layouts: Ensure scale animations position correctly in both directions
  7. Use easeOutBack for bounce: Creates satisfying spring effect on scale up
  8. Avoid excessive scaling: Keep within 0.8-1.2 range for most UI elements
  9. Consider reduced motion: Respect user preferences for reduced animations
  10. Maintain touch targets: Ensure scaled-down elements remain tappable

Conclusion

AnimatedScale is a versatile widget for creating smooth scaling animations in multilingual Flutter apps. By providing appropriate accessibility announcements, using subtle scale values for feedback, and combining with other visual effects, you create intuitive experiences for users worldwide. The patterns shown here—interactive buttons, selectable cards, image zoom, and notification badges—can be adapted for any application requiring animated scaling transitions.

Remember to test your scale animations with various screen sizes and accessibility settings to ensure they remain effective and accessible for all users.