← Back to Blog

Flutter SimpleDialog Localization: Selection Dialogs for Multilingual Apps

fluttersimpledialogdialogselectionlocalizationrtl

Flutter SimpleDialog Localization: Selection Dialogs for Multilingual Apps

SimpleDialog is a Flutter widget that presents a list of options in a dialog for the user to choose from. In multilingual applications, SimpleDialog is essential for displaying translated option lists in a modal dialog, providing localized dialog titles that describe the selection, supporting RTL dialog layout with correctly aligned options, and building accessible selection dialogs with announcements in the active language.

Understanding SimpleDialog in Localization Context

SimpleDialog renders a Material Design dialog with a title and a list of SimpleDialogOption children. For multilingual apps, this enables:

  • Translated option labels in a scrollable selection list
  • Localized dialog titles that describe what the user is choosing
  • RTL-aware option alignment and icon positioning
  • Accessible option selection announced in the active language

Why SimpleDialog Matters for Multilingual Apps

SimpleDialog provides:

  • Option selection: Translated choices in a standard dialog format
  • Scrollable list: Handles many translated options without overflow
  • Simple API: Straightforward title and options with localized labels
  • Return value: Selected option returned as a Future for programmatic handling

Basic SimpleDialog Implementation

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

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

  @override
  State<LocalizedSimpleDialogExample> createState() =>
      _LocalizedSimpleDialogExampleState();
}

class _LocalizedSimpleDialogExampleState
    extends State<LocalizedSimpleDialogExample> {
  String? _selectedLanguage;

  Future<void> _showLanguageDialog() async {
    final l10n = AppLocalizations.of(context)!;

    final result = await showDialog<String>(
      context: context,
      builder: (context) {
        return SimpleDialog(
          title: Text(l10n.selectLanguageTitle),
          children: [
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'en'),
              child: Text(l10n.englishLanguage),
            ),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'ar'),
              child: Text(l10n.arabicLanguage),
            ),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'de'),
              child: Text(l10n.germanLanguage),
            ),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'es'),
              child: Text(l10n.spanishLanguage),
            ),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'fr'),
              child: Text(l10n.frenchLanguage),
            ),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'ja'),
              child: Text(l10n.japaneseLanguage),
            ),
          ],
        );
      },
    );

    if (result != null) {
      setState(() => _selectedLanguage = result);
    }
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.settingsTitle)),
      body: ListView(
        children: [
          ListTile(
            leading: const Icon(Icons.language),
            title: Text(l10n.languageLabel),
            subtitle: Text(_selectedLanguage ?? l10n.systemDefaultLabel),
            onTap: _showLanguageDialog,
          ),
        ],
      ),
    );
  }
}

Advanced SimpleDialog Patterns for Localization

Options with Icons and Descriptions

SimpleDialog options with leading icons and translated descriptions for each choice.

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

  Future<void> _showThemeDialog(BuildContext context) async {
    final l10n = AppLocalizations.of(context)!;

    await showDialog<String>(
      context: context,
      builder: (context) {
        return SimpleDialog(
          title: Text(l10n.selectThemeTitle),
          children: [
            _buildOption(
              context,
              icon: Icons.brightness_auto,
              title: l10n.systemThemeLabel,
              subtitle: l10n.systemThemeDescription,
              value: 'system',
            ),
            _buildOption(
              context,
              icon: Icons.light_mode,
              title: l10n.lightThemeLabel,
              subtitle: l10n.lightThemeDescription,
              value: 'light',
            ),
            _buildOption(
              context,
              icon: Icons.dark_mode,
              title: l10n.darkThemeLabel,
              subtitle: l10n.darkThemeDescription,
              value: 'dark',
            ),
          ],
        );
      },
    );
  }

  Widget _buildOption(
    BuildContext context, {
    required IconData icon,
    required String title,
    required String subtitle,
    required String value,
  }) {
    return SimpleDialogOption(
      onPressed: () => Navigator.pop(context, value),
      child: Row(
        children: [
          Icon(icon, size: 28),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleSmall,
                ),
                Text(
                  subtitle,
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.displaySettingsTitle)),
      body: ListView(
        children: [
          ListTile(
            leading: const Icon(Icons.palette),
            title: Text(l10n.themeLabel),
            onTap: () => _showThemeDialog(context),
          ),
        ],
      ),
    );
  }
}

Sort Selection Dialog

A SimpleDialog for selecting sort order with translated sort options.

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

  @override
  State<SortSelectionDialog> createState() => _SortSelectionDialogState();
}

class _SortSelectionDialogState extends State<SortSelectionDialog> {
  String _currentSort = 'name_asc';

  Future<void> _showSortDialog() async {
    final l10n = AppLocalizations.of(context)!;

    final sortOptions = [
      ('name_asc', l10n.nameAscLabel, Icons.sort_by_alpha),
      ('name_desc', l10n.nameDescLabel, Icons.sort_by_alpha),
      ('date_newest', l10n.dateNewestLabel, Icons.calendar_today),
      ('date_oldest', l10n.dateOldestLabel, Icons.calendar_today),
      ('size_largest', l10n.sizeLargestLabel, Icons.storage),
      ('size_smallest', l10n.sizeSmallestLabel, Icons.storage),
    ];

    final result = await showDialog<String>(
      context: context,
      builder: (context) {
        return SimpleDialog(
          title: Text(l10n.sortByTitle),
          children: sortOptions.map((option) {
            final (value, label, icon) = option;
            final isSelected = value == _currentSort;

            return SimpleDialogOption(
              onPressed: () => Navigator.pop(context, value),
              child: Row(
                children: [
                  Icon(
                    icon,
                    color: isSelected
                        ? Theme.of(context).colorScheme.primary
                        : null,
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Text(
                      label,
                      style: isSelected
                          ? TextStyle(
                              color: Theme.of(context).colorScheme.primary,
                              fontWeight: FontWeight.bold,
                            )
                          : null,
                    ),
                  ),
                  if (isSelected)
                    Icon(
                      Icons.check,
                      color: Theme.of(context).colorScheme.primary,
                    ),
                ],
              ),
            );
          }).toList(),
        );
      },
    );

    if (result != null) {
      setState(() => _currentSort = result);
    }
  }

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.filesTitle),
        actions: [
          IconButton(
            icon: const Icon(Icons.sort),
            tooltip: l10n.sortTooltip,
            onPressed: _showSortDialog,
          ),
        ],
      ),
      body: const Center(child: Placeholder()),
    );
  }
}

Account Switcher Dialog

A SimpleDialog for switching between user accounts with translated labels.

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

  Future<void> _showAccountDialog(BuildContext context) async {
    final l10n = AppLocalizations.of(context)!;

    await showDialog<String>(
      context: context,
      builder: (context) {
        return SimpleDialog(
          title: Text(l10n.switchAccountTitle),
          children: [
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'personal'),
              child: Row(
                children: [
                  CircleAvatar(
                    backgroundColor:
                        Theme.of(context).colorScheme.primaryContainer,
                    child: const Text('P'),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(l10n.personalAccountLabel),
                        Text(
                          'personal@example.com',
                          style: Theme.of(context).textTheme.bodySmall,
                        ),
                      ],
                    ),
                  ),
                  const Icon(Icons.check, size: 20),
                ],
              ),
            ),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'work'),
              child: Row(
                children: [
                  CircleAvatar(
                    backgroundColor:
                        Theme.of(context).colorScheme.tertiaryContainer,
                    child: const Text('W'),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(l10n.workAccountLabel),
                        Text(
                          'work@company.com',
                          style: Theme.of(context).textTheme.bodySmall,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            const Divider(),
            SimpleDialogOption(
              onPressed: () => Navigator.pop(context, 'add'),
              child: Row(
                children: [
                  const CircleAvatar(child: Icon(Icons.add)),
                  const SizedBox(width: 16),
                  Text(l10n.addAccountLabel),
                ],
              ),
            ),
          ],
        );
      },
    );
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.profileTitle)),
      body: ListView(
        children: [
          ListTile(
            leading: const CircleAvatar(child: Text('P')),
            title: Text(l10n.personalAccountLabel),
            subtitle: Text(l10n.switchAccountSubtitle),
            onTap: () => _showAccountDialog(context),
          ),
        ],
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

SimpleDialog automatically handles RTL layouts. The title and options align from right to left, and leading icons swap to the correct side.

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.settingsTitle)),
      body: Center(
        child: FilledButton(
          onPressed: () {
            showDialog(
              context: context,
              builder: (context) {
                return SimpleDialog(
                  title: Text(l10n.selectCurrencyTitle),
                  children: [
                    SimpleDialogOption(
                      onPressed: () => Navigator.pop(context),
                      child: Text(l10n.usDollarLabel),
                    ),
                    SimpleDialogOption(
                      onPressed: () => Navigator.pop(context),
                      child: Text(l10n.euroLabel),
                    ),
                    SimpleDialogOption(
                      onPressed: () => Navigator.pop(context),
                      child: Text(l10n.saudiRiyalLabel),
                    ),
                  ],
                );
              },
            );
          },
          child: Text(l10n.changeCurrencyLabel),
        ),
      ),
    );
  }
}

Testing SimpleDialog Localization

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

void main() {
  Widget buildTestWidget({Locale locale = const Locale('en')}) {
    return MaterialApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedSimpleDialogExample(),
    );
  }

  testWidgets('SimpleDialog shows localized options', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    await tester.tap(find.byType(ListTile));
    await tester.pumpAndSettle();
    expect(find.byType(SimpleDialog), findsOneWidget);
  });

  testWidgets('SimpleDialog works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Use showDialog<T> with a return type to capture the user's translated selection programmatically.

  2. Provide a clear translated title that tells the user what they are selecting, like "Select language" or "Sort by".

  3. Highlight the current selection with a check icon and colored text so users can see their active choice in any language.

  4. Add icons to options for visual context alongside translated labels, helping users identify options across languages.

  5. Use Divider to separate option groups within the dialog, such as existing accounts from "Add account".

  6. Test with verbose translations to verify the dialog scrolls correctly when option labels are longer than expected.

Conclusion

SimpleDialog provides a straightforward option selection dialog for Flutter apps. For multilingual apps, it handles translated option labels and titles, supports highlighted current selections, and automatically adapts to RTL layouts. By combining SimpleDialog with icon-labeled options, sort selectors, and account switchers, you can build selection interfaces that present translated choices clearly in every supported language.

Further Reading