← Back to Blog

Flutter AnimatedPositioned Localization: Sliding Panels, Dynamic Positioning, and RTL Animations

flutteranimatedpositionedanimationpositioninglocalizationrtl

Flutter AnimatedPositioned Localization: Sliding Panels, Dynamic Positioning, and RTL Animations

AnimatedPositioned automatically animates changes to a widget's position within a Stack. Proper localization ensures that sliding panels, floating elements, and dynamic positioning work seamlessly across languages and text directions. This guide covers comprehensive strategies for localizing AnimatedPositioned widgets in Flutter.

Understanding AnimatedPositioned Localization

AnimatedPositioned widgets require localization for:

  • RTL positioning: Mirroring left/right positions for right-to-left languages
  • Dynamic offsets: Adjusting positions based on localized content width
  • Sliding panels: Localizing slide-in menus and drawers
  • Floating elements: Positioning tooltips and badges near localized text
  • Accessibility labels: Screen reader announcements for position changes
  • Directional animations: Ensuring animations flow correctly in RTL layouts

Basic AnimatedPositioned with RTL Support

Start with a simple AnimatedPositioned that adapts to text direction:

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

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

  @override
  State<LocalizedSlidingPanel> createState() => _LocalizedSlidingPanelState();
}

class _LocalizedSlidingPanelState extends State<LocalizedSlidingPanel> {
  bool _isPanelOpen = false;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final isRtl = Directionality.of(context) == TextDirection.rtl;
    final screenWidth = MediaQuery.of(context).size.width;
    final panelWidth = screenWidth * 0.75;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.slidingPanelTitle),
        leading: IconButton(
          icon: const Icon(Icons.menu),
          onPressed: () => setState(() => _isPanelOpen = !_isPanelOpen),
          tooltip: _isPanelOpen ? l10n.closePanel : l10n.openPanel,
        ),
      ),
      body: Stack(
        children: [
          // Main content
          GestureDetector(
            onTap: () {
              if (_isPanelOpen) setState(() => _isPanelOpen = false);
            },
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 300),
              color: _isPanelOpen
                  ? Colors.black.withOpacity(0.3)
                  : Colors.transparent,
              child: Center(
                child: Text(l10n.mainContent),
              ),
            ),
          ),
          // Sliding panel
          AnimatedPositioned(
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeOutCubic,
            top: 0,
            bottom: 0,
            left: isRtl
                ? null
                : (_isPanelOpen ? 0 : -panelWidth),
            right: isRtl
                ? (_isPanelOpen ? 0 : -panelWidth)
                : null,
            width: panelWidth,
            child: Semantics(
              label: _isPanelOpen
                  ? l10n.panelOpenAccessibility
                  : l10n.panelClosedAccessibility,
              child: Material(
                elevation: _isPanelOpen ? 8 : 0,
                child: Container(
                  color: Theme.of(context).colorScheme.surface,
                  child: SafeArea(
                    child: Column(
                      crossAxisAlignment: isRtl
                          ? CrossAxisAlignment.end
                          : CrossAxisAlignment.start,
                      children: [
                        Padding(
                          padding: const EdgeInsets.all(16),
                          child: Text(
                            l10n.menuTitle,
                            style: Theme.of(context).textTheme.headlineSmall,
                          ),
                        ),
                        const Divider(),
                        _buildMenuItem(l10n.menuHome, Icons.home),
                        _buildMenuItem(l10n.menuProfile, Icons.person),
                        _buildMenuItem(l10n.menuSettings, Icons.settings),
                        _buildMenuItem(l10n.menuHelp, Icons.help),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildMenuItem(String label, IconData icon) {
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return ListTile(
      leading: isRtl ? null : Icon(icon),
      trailing: isRtl ? Icon(icon) : null,
      title: Text(label),
      onTap: () => setState(() => _isPanelOpen = false),
    );
  }
}

ARB File Structure for AnimatedPositioned

{
  "slidingPanelTitle": "Sliding Panel",
  "@slidingPanelTitle": {
    "description": "Title for the sliding panel screen"
  },
  "openPanel": "Open menu panel",
  "@openPanel": {
    "description": "Tooltip for opening the panel"
  },
  "closePanel": "Close menu panel",
  "@closePanel": {
    "description": "Tooltip for closing the panel"
  },
  "mainContent": "Main content area",
  "@mainContent": {
    "description": "Placeholder text for main content"
  },
  "panelOpenAccessibility": "Navigation panel is open",
  "@panelOpenAccessibility": {
    "description": "Accessibility label when panel is open"
  },
  "panelClosedAccessibility": "Navigation panel is closed",
  "@panelClosedAccessibility": {
    "description": "Accessibility label when panel is closed"
  },
  "menuTitle": "Navigation",
  "@menuTitle": {
    "description": "Title of the navigation menu"
  },
  "menuHome": "Home",
  "@menuHome": {
    "description": "Home menu item"
  },
  "menuProfile": "Profile",
  "@menuProfile": {
    "description": "Profile menu item"
  },
  "menuSettings": "Settings",
  "@menuSettings": {
    "description": "Settings menu item"
  },
  "menuHelp": "Help",
  "@menuHelp": {
    "description": "Help menu item"
  }
}

Floating Action Button with Dynamic Position

Create a FAB that animates to different positions:

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

enum FabPosition { bottomRight, bottomLeft, topRight, topLeft, center }

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

  @override
  State<LocalizedAnimatedFab> createState() => _LocalizedAnimatedFabState();
}

class _LocalizedAnimatedFabState extends State<LocalizedAnimatedFab> {
  FabPosition _position = FabPosition.bottomRight;

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

    final positions = _calculatePositions(size, isRtl);

    return Scaffold(
      appBar: AppBar(title: Text(l10n.fabPositionTitle)),
      body: Stack(
        children: [
          // Position selector
          Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  l10n.selectPosition,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 16),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: FabPosition.values.map((pos) {
                    return ChoiceChip(
                      label: Text(_getPositionLabel(l10n, pos)),
                      selected: _position == pos,
                      onSelected: (_) => setState(() => _position = pos),
                    );
                  }).toList(),
                ),
              ],
            ),
          ),
          // Animated FAB
          AnimatedPositioned(
            duration: const Duration(milliseconds: 400),
            curve: Curves.easeInOutCubic,
            top: positions[_position]!.top,
            bottom: positions[_position]!.bottom,
            left: positions[_position]!.left,
            right: positions[_position]!.right,
            child: Semantics(
              button: true,
              label: l10n.fabAccessibility(_getPositionLabel(l10n, _position)),
              child: FloatingActionButton(
                onPressed: () => _showSnackBar(l10n),
                tooltip: l10n.fabTooltip,
                child: const Icon(Icons.add),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Map<FabPosition, _PositionOffsets> _calculatePositions(Size size, bool isRtl) {
    const padding = 16.0;
    const fabSize = 56.0;
    final centerX = (size.width - fabSize) / 2;
    final centerY = (size.height - fabSize - kToolbarHeight) / 2;

    // Swap left/right for RTL
    final startPadding = isRtl ? null : padding;
    final endPadding = isRtl ? padding : null;
    final startNull = isRtl ? padding : null;
    final endNull = isRtl ? null : padding;

    return {
      FabPosition.bottomRight: _PositionOffsets(
        bottom: padding,
        right: endNull,
        left: startNull,
      ),
      FabPosition.bottomLeft: _PositionOffsets(
        bottom: padding,
        left: startPadding,
        right: endPadding,
      ),
      FabPosition.topRight: _PositionOffsets(
        top: padding,
        right: endNull,
        left: startNull,
      ),
      FabPosition.topLeft: _PositionOffsets(
        top: padding,
        left: startPadding,
        right: endPadding,
      ),
      FabPosition.center: _PositionOffsets(
        top: centerY,
        left: centerX,
      ),
    };
  }

  String _getPositionLabel(AppLocalizations l10n, FabPosition position) {
    return switch (position) {
      FabPosition.bottomRight => l10n.positionBottomEnd,
      FabPosition.bottomLeft => l10n.positionBottomStart,
      FabPosition.topRight => l10n.positionTopEnd,
      FabPosition.topLeft => l10n.positionTopStart,
      FabPosition.center => l10n.positionCenter,
    };
  }

  void _showSnackBar(AppLocalizations l10n) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(l10n.fabPressed)),
    );
  }
}

class _PositionOffsets {
  final double? top;
  final double? bottom;
  final double? left;
  final double? right;

  _PositionOffsets({this.top, this.bottom, this.left, this.right});
}

Tooltip Positioning Based on Content

Create tooltips that position correctly for localized text:

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

class LocalizedAnimatedTooltip extends StatefulWidget {
  final String targetText;
  final String tooltipText;
  final Widget child;

  const LocalizedAnimatedTooltip({
    super.key,
    required this.targetText,
    required this.tooltipText,
    required this.child,
  });

  @override
  State<LocalizedAnimatedTooltip> createState() => _LocalizedAnimatedTooltipState();
}

class _LocalizedAnimatedTooltipState extends State<LocalizedAnimatedTooltip> {
  bool _showTooltip = false;
  final GlobalKey _targetKey = GlobalKey();

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

    return Stack(
      clipBehavior: Clip.none,
      children: [
        // Target widget
        GestureDetector(
          key: _targetKey,
          onTap: () => setState(() => _showTooltip = !_showTooltip),
          onLongPress: () => setState(() => _showTooltip = true),
          child: widget.child,
        ),
        // Animated tooltip
        if (_showTooltip)
          Positioned(
            top: -48,
            left: isRtl ? null : 0,
            right: isRtl ? 0 : null,
            child: TweenAnimationBuilder<double>(
              duration: const Duration(milliseconds: 200),
              tween: Tween(begin: 0, end: 1),
              builder: (context, value, child) {
                return Opacity(
                  opacity: value,
                  child: Transform.translate(
                    offset: Offset(0, (1 - value) * 8),
                    child: child,
                  ),
                );
              },
              child: GestureDetector(
                onTap: () => setState(() => _showTooltip = false),
                child: Semantics(
                  liveRegion: true,
                  label: l10n.tooltipShowing(widget.tooltipText),
                  child: Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 12,
                      vertical: 8,
                    ),
                    decoration: BoxDecoration(
                      color: Theme.of(context).colorScheme.inverseSurface,
                      borderRadius: BorderRadius.circular(8),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withOpacity(0.2),
                          blurRadius: 8,
                          offset: const Offset(0, 4),
                        ),
                      ],
                    ),
                    child: Text(
                      widget.tooltipText,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onInverseSurface,
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

Notification Badge with Animated Position

Create a badge that animates its position:

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

class LocalizedAnimatedBadge extends StatelessWidget {
  final int count;
  final Widget child;
  final bool showBadge;

  const LocalizedAnimatedBadge({
    super.key,
    required this.count,
    required this.child,
    this.showBadge = true,
  });

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

    return Stack(
      clipBehavior: Clip.none,
      children: [
        child,
        AnimatedPositioned(
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOutBack,
          top: showBadge && count > 0 ? -4 : 0,
          right: isRtl ? null : (showBadge && count > 0 ? -4 : 8),
          left: isRtl ? (showBadge && count > 0 ? -4 : 8) : null,
          child: AnimatedScale(
            duration: const Duration(milliseconds: 200),
            scale: showBadge && count > 0 ? 1 : 0,
            child: Semantics(
              label: l10n.notificationCount(count),
              child: Container(
                padding: const EdgeInsets.all(4),
                constraints: const BoxConstraints(
                  minWidth: 20,
                  minHeight: 20,
                ),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.error,
                  shape: count > 9 ? BoxShape.rectangle : BoxShape.circle,
                  borderRadius: count > 9 ? BorderRadius.circular(10) : null,
                  border: Border.all(
                    color: Theme.of(context).colorScheme.surface,
                    width: 2,
                  ),
                ),
                child: Center(
                  child: Text(
                    count > 99 ? l10n.badgeOverflow : count.toString(),
                    style: Theme.of(context).textTheme.labelSmall?.copyWith(
                      color: Theme.of(context).colorScheme.onError,
                      fontWeight: FontWeight.bold,
                      fontSize: 10,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

// Usage example
class NotificationIconExample extends StatefulWidget {
  const NotificationIconExample({super.key});

  @override
  State<NotificationIconExample> createState() => _NotificationIconExampleState();
}

class _NotificationIconExampleState extends State<NotificationIconExample> {
  int _notificationCount = 3;

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

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        LocalizedAnimatedBadge(
          count: _notificationCount,
          child: IconButton(
            icon: const Icon(Icons.notifications),
            onPressed: () {},
            tooltip: l10n.notificationsTooltip,
          ),
        ),
        const SizedBox(height: 16),
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              onPressed: () => setState(() => _notificationCount++),
              icon: const Icon(Icons.add),
              tooltip: l10n.addNotification,
            ),
            IconButton(
              onPressed: () => setState(() {
                if (_notificationCount > 0) _notificationCount--;
              }),
              icon: const Icon(Icons.remove),
              tooltip: l10n.removeNotification,
            ),
            IconButton(
              onPressed: () => setState(() => _notificationCount = 0),
              icon: const Icon(Icons.clear),
              tooltip: l10n.clearNotifications,
            ),
          ],
        ),
      ],
    );
  }
}

Draggable Positioned Element

Create a draggable element with snap-to-position:

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

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

  @override
  State<LocalizedDraggablePositioned> createState() =>
      _LocalizedDraggablePositionedState();
}

class _LocalizedDraggablePositionedState
    extends State<LocalizedDraggablePositioned> {
  Offset _position = const Offset(100, 100);
  bool _isDragging = false;
  final List<Offset> _snapPoints = [];

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _updateSnapPoints();
  }

  void _updateSnapPoints() {
    final size = MediaQuery.of(context).size;
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    // Define snap points that respect RTL
    final startX = isRtl ? size.width - 80 : 20.0;
    final endX = isRtl ? 20.0 : size.width - 80;

    _snapPoints.clear();
    _snapPoints.addAll([
      Offset(startX, 100), // Top start
      Offset(endX, 100), // Top end
      Offset(size.width / 2 - 30, size.height / 2 - 30), // Center
      Offset(startX, size.height - 180), // Bottom start
      Offset(endX, size.height - 180), // Bottom end
    ]);
  }

  Offset _findNearestSnapPoint(Offset position) {
    Offset nearest = _snapPoints.first;
    double minDistance = double.infinity;

    for (final point in _snapPoints) {
      final distance = (position - point).distance;
      if (distance < minDistance) {
        minDistance = distance;
        nearest = point;
      }
    }

    return nearest;
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.draggableTitle)),
      body: Stack(
        children: [
          // Snap point indicators
          ..._snapPoints.map((point) => Positioned(
            left: point.dx,
            top: point.dy,
            child: Container(
              width: 60,
              height: 60,
              decoration: BoxDecoration(
                border: Border.all(
                  color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
                  width: 2,
                  style: BorderStyle.solid,
                ),
                borderRadius: BorderRadius.circular(12),
              ),
            ),
          )),
          // Instructions
          Positioned(
            top: 16,
            left: isRtl ? null : 16,
            right: isRtl ? 16 : null,
            child: Text(
              l10n.dragInstructions,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Theme.of(context).colorScheme.outline,
              ),
            ),
          ),
          // Draggable element
          AnimatedPositioned(
            duration: _isDragging
                ? Duration.zero
                : const Duration(milliseconds: 300),
            curve: Curves.easeOutCubic,
            left: _position.dx,
            top: _position.dy,
            child: GestureDetector(
              onPanStart: (_) => setState(() => _isDragging = true),
              onPanUpdate: (details) {
                setState(() {
                  _position += details.delta;
                });
              },
              onPanEnd: (_) {
                setState(() {
                  _isDragging = false;
                  _position = _findNearestSnapPoint(_position);
                });
              },
              child: Semantics(
                label: l10n.draggableElementAccessibility,
                hint: l10n.draggableHint,
                child: AnimatedContainer(
                  duration: const Duration(milliseconds: 150),
                  width: 60,
                  height: 60,
                  decoration: BoxDecoration(
                    color: _isDragging
                        ? Theme.of(context).colorScheme.primaryContainer
                        : Theme.of(context).colorScheme.primary,
                    borderRadius: BorderRadius.circular(12),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withOpacity(_isDragging ? 0.3 : 0.2),
                        blurRadius: _isDragging ? 16 : 8,
                        offset: Offset(0, _isDragging ? 8 : 4),
                      ),
                    ],
                  ),
                  child: Icon(
                    Icons.drag_indicator,
                    color: _isDragging
                        ? Theme.of(context).colorScheme.primary
                        : Theme.of(context).colorScheme.onPrimary,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Animated Tab Indicator

Create a tab indicator that slides between positions:

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

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

  @override
  State<LocalizedAnimatedTabIndicator> createState() =>
      _LocalizedAnimatedTabIndicatorState();
}

class _LocalizedAnimatedTabIndicatorState
    extends State<LocalizedAnimatedTabIndicator> {
  int _selectedIndex = 0;
  final List<GlobalKey> _tabKeys = List.generate(4, (_) => GlobalKey());

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

    final tabs = [
      _TabItem(l10n.tabHome, Icons.home),
      _TabItem(l10n.tabSearch, Icons.search),
      _TabItem(l10n.tabFavorites, Icons.favorite),
      _TabItem(l10n.tabProfile, Icons.person),
    ];

    return Column(
      children: [
        Container(
          height: 64,
          margin: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.surfaceVariant,
            borderRadius: BorderRadius.circular(16),
          ),
          child: LayoutBuilder(
            builder: (context, constraints) {
              final tabWidth = constraints.maxWidth / tabs.length;

              return Stack(
                children: [
                  // Animated indicator
                  AnimatedPositioned(
                    duration: const Duration(milliseconds: 250),
                    curve: Curves.easeOutCubic,
                    top: 4,
                    bottom: 4,
                    left: isRtl
                        ? null
                        : _selectedIndex * tabWidth + 4,
                    right: isRtl
                        ? _selectedIndex * tabWidth + 4
                        : null,
                    width: tabWidth - 8,
                    child: Container(
                      decoration: BoxDecoration(
                        color: Theme.of(context).colorScheme.primary,
                        borderRadius: BorderRadius.circular(12),
                        boxShadow: [
                          BoxShadow(
                            color: Theme.of(context)
                                .colorScheme
                                .primary
                                .withOpacity(0.3),
                            blurRadius: 8,
                            offset: const Offset(0, 2),
                          ),
                        ],
                      ),
                    ),
                  ),
                  // Tab buttons
                  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.tabAccessibility(
                            tab.label,
                            index + 1,
                            tabs.length,
                          ),
                          child: GestureDetector(
                            key: _tabKeys[index],
                            onTap: () => setState(() => _selectedIndex = index),
                            behavior: HitTestBehavior.opaque,
                            child: Center(
                              child: AnimatedDefaultTextStyle(
                                duration: const Duration(milliseconds: 200),
                                style: Theme.of(context)
                                    .textTheme
                                    .labelLarge!
                                    .copyWith(
                                  color: isSelected
                                      ? Theme.of(context).colorScheme.onPrimary
                                      : Theme.of(context)
                                          .colorScheme
                                          .onSurfaceVariant,
                                  fontWeight: isSelected
                                      ? FontWeight.bold
                                      : FontWeight.normal,
                                ),
                                child: Row(
                                  mainAxisSize: MainAxisSize.min,
                                  children: [
                                    Icon(
                                      tab.icon,
                                      size: 20,
                                      color: isSelected
                                          ? Theme.of(context)
                                              .colorScheme
                                              .onPrimary
                                          : Theme.of(context)
                                              .colorScheme
                                              .onSurfaceVariant,
                                    ),
                                    const SizedBox(width: 4),
                                    Text(tab.label),
                                  ],
                                ),
                              ),
                            ),
                          ),
                        ),
                      );
                    }).toList(),
                  ),
                ],
              );
            },
          ),
        ),
        // Content area
        Expanded(
          child: Center(
            child: Text(
              l10n.tabContent(tabs[_selectedIndex].label),
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ),
        ),
      ],
    );
  }
}

class _TabItem {
  final String label;
  final IconData icon;

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

Complete ARB File for AnimatedPositioned

{
  "@@locale": "en",

  "slidingPanelTitle": "Sliding Panel",
  "@slidingPanelTitle": {
    "description": "Title for the sliding panel demo"
  },
  "openPanel": "Open menu panel",
  "@openPanel": {
    "description": "Tooltip for opening the panel"
  },
  "closePanel": "Close menu panel",
  "@closePanel": {
    "description": "Tooltip for closing the panel"
  },
  "mainContent": "Main content area",
  "@mainContent": {
    "description": "Main content placeholder"
  },
  "panelOpenAccessibility": "Navigation panel is open",
  "@panelOpenAccessibility": {
    "description": "Accessibility label for open panel"
  },
  "panelClosedAccessibility": "Navigation panel is closed",
  "@panelClosedAccessibility": {
    "description": "Accessibility label for closed panel"
  },
  "menuTitle": "Navigation",
  "@menuTitle": {
    "description": "Navigation menu title"
  },
  "menuHome": "Home",
  "menuProfile": "Profile",
  "menuSettings": "Settings",
  "menuHelp": "Help",

  "fabPositionTitle": "FAB Position",
  "@fabPositionTitle": {
    "description": "Title for FAB position demo"
  },
  "selectPosition": "Select FAB position:",
  "@selectPosition": {
    "description": "Instruction to select position"
  },
  "positionBottomEnd": "Bottom End",
  "positionBottomStart": "Bottom Start",
  "positionTopEnd": "Top End",
  "positionTopStart": "Top Start",
  "positionCenter": "Center",
  "fabAccessibility": "Add button at {position}",
  "@fabAccessibility": {
    "description": "FAB accessibility label",
    "placeholders": {
      "position": {"type": "String"}
    }
  },
  "fabTooltip": "Add item",
  "fabPressed": "Button pressed",

  "tooltipShowing": "Tooltip: {text}",
  "@tooltipShowing": {
    "description": "Accessibility for showing tooltip",
    "placeholders": {
      "text": {"type": "String"}
    }
  },

  "notificationCount": "{count} {count, plural, =0{notifications} =1{notification} other{notifications}}",
  "@notificationCount": {
    "description": "Notification count label",
    "placeholders": {
      "count": {"type": "int"}
    }
  },
  "badgeOverflow": "99+",
  "@badgeOverflow": {
    "description": "Badge text when count exceeds 99"
  },
  "notificationsTooltip": "Notifications",
  "addNotification": "Add notification",
  "removeNotification": "Remove notification",
  "clearNotifications": "Clear all notifications",

  "draggableTitle": "Draggable Element",
  "dragInstructions": "Drag the element to snap points",
  "draggableElementAccessibility": "Draggable element",
  "draggableHint": "Double-tap and hold to drag",

  "tabHome": "Home",
  "tabSearch": "Search",
  "tabFavorites": "Favorites",
  "tabProfile": "Profile",
  "tabAccessibility": "{label}, tab {current} of {total}",
  "@tabAccessibility": {
    "description": "Tab accessibility label",
    "placeholders": {
      "label": {"type": "String"},
      "current": {"type": "int"},
      "total": {"type": "int"}
    }
  },
  "tabContent": "{tab} content",
  "@tabContent": {
    "description": "Tab content placeholder",
    "placeholders": {
      "tab": {"type": "String"}
    }
  }
}

Best Practices Summary

  1. Mirror positions for RTL: Swap left/right properties based on text direction
  2. Use directional terminology: Say "start/end" instead of "left/right" in user-facing text
  3. Calculate positions dynamically: Account for different text lengths in positioning
  4. Provide accessibility labels: Announce position changes for screen readers
  5. Use semantic positioning: Reference logical positions rather than absolute coordinates
  6. Handle edge cases: Ensure elements don't overflow screen boundaries in any locale
  7. Test with RTL languages: Verify animations flow correctly in right-to-left layouts
  8. Use appropriate curves: Choose animation curves that feel natural for the direction of movement
  9. Consider text expansion: Languages like German expand 30%+ compared to English
  10. Snap to logical positions: Use locale-aware snap points for draggable elements

Conclusion

Proper AnimatedPositioned localization ensures smooth, directionally-correct animations across all languages. By handling RTL layouts, calculating dynamic positions, and providing comprehensive accessibility labels, you create polished sliding and positioning animations that feel native to users worldwide. The patterns shown here—sliding panels, positioned FABs, badges, and tab indicators—can be adapted to any Flutter application requiring animated positioning.

Remember to test your positioned animations with different locales to verify that sliding directions, snap points, and accessibility work correctly for all your supported languages.