← Back to Blog

Flutter SelectableText Localization: User-Copyable Multilingual Content

flutterselectabletexttextselectionlocalizationrtl

Flutter SelectableText Localization: User-Copyable Multilingual Content

SelectableText is a Flutter widget that displays a string of text that users can select and copy. In multilingual applications, SelectableText is essential for content that users need to reference, copy, or share -- such as addresses, codes, legal text, and translated quotes -- because it must handle bidirectional text selection, script-appropriate selection handles, and varying text lengths across languages.

Understanding SelectableText in Localization Context

SelectableText renders selectable, non-editable text with automatic text direction based on the ambient locale. For multilingual apps, this enables:

  • Bidirectional text selection with correct handle placement for RTL scripts
  • Script-appropriate selection highlighting for Arabic, Hebrew, CJK, and Devanagari
  • Copy-to-clipboard support with locale-aware text boundaries
  • Overflow handling for selectable content in constrained layouts

Why SelectableText Matters for Multilingual Apps

SelectableText provides:

  • Selectable translations: Users can copy translated content for reference or sharing
  • BiDi selection: Selection handles position correctly in RTL and mixed-direction text
  • Accessibility: Screen readers announce selectable content with appropriate semantics
  • Style inheritance: Inherits DefaultTextStyle for consistent localized typography

Basic SelectableText Implementation

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

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.referenceCodeLabel,
              style: Theme.of(context).textTheme.labelMedium,
            ),
            const SizedBox(height: 4),
            SelectableText(
              l10n.referenceCode,
              style: Theme.of(context).textTheme.titleLarge?.copyWith(
                fontFamily: 'monospace',
                letterSpacing: 2,
              ),
            ),
            const SizedBox(height: 24),
            Text(
              l10n.addressLabel,
              style: Theme.of(context).textTheme.labelMedium,
            ),
            const SizedBox(height: 4),
            SelectableText(
              l10n.companyAddress,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ],
        ),
      ),
    );
  }
}

Advanced SelectableText Patterns for Localization

Selectable Legal and Policy Text

Legal content must be selectable in all languages so users can copy terms for their records. Long legal translations require scroll support.

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.termsOfServiceTitle)),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SelectableText(
              l10n.termsOfServiceHeading,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 8),
            SelectableText(
              l10n.termsLastUpdated,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 16),
            SelectableText(
              l10n.termsOfServiceBody,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                height: 1.6,
              ),
            ),
            const SizedBox(height: 24),
            SelectableText(
              l10n.privacyPolicyHeading,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            SelectableText(
              l10n.privacyPolicyBody,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                height: 1.6,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

SelectableText.rich for Mixed-Style Selectable Content

When selectable content needs inline styling variations, SelectableText.rich renders a styled TextSpan tree that remains fully selectable.

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

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

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            l10n.quoteOfTheDayLabel,
            style: Theme.of(context).textTheme.labelMedium,
          ),
          const SizedBox(height: 8),
          SelectableText.rich(
            TextSpan(
              children: [
                TextSpan(
                  text: l10n.quoteText,
                  style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                    fontStyle: FontStyle.italic,
                    height: 1.6,
                  ),
                ),
                TextSpan(
                  text: '\n— ${l10n.quoteAuthor}',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
              ],
            ),
            textAlign: TextAlign.start,
          ),
          const SizedBox(height: 24),
          SelectableText.rich(
            TextSpan(
              children: [
                TextSpan(text: l10n.contactPrefix),
                TextSpan(
                  text: ' support@example.com ',
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.primary,
                    decoration: TextDecoration.underline,
                  ),
                ),
                TextSpan(text: l10n.contactSuffix),
              ],
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
        ],
      ),
    );
  }
}

Locale-Aware Selection Behavior

Different scripts have different word boundaries. SelectableText respects Unicode text segmentation, but you may want to adjust line height and padding for readability during selection.

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

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

    TextStyle baseStyle = Theme.of(context).textTheme.bodyLarge!;

    if (['ar', 'he', 'fa'].contains(locale.languageCode)) {
      baseStyle = baseStyle.copyWith(fontSize: 17, height: 1.8);
    } else if (['zh', 'ja', 'ko'].contains(locale.languageCode)) {
      baseStyle = baseStyle.copyWith(height: 1.6);
    } else if (['th', 'hi', 'bn'].contains(locale.languageCode)) {
      baseStyle = baseStyle.copyWith(height: 1.7);
    }

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: SelectableText(
          l10n.articleContent,
          style: baseStyle,
          textAlign: TextAlign.start,
        ),
      ),
    );
  }
}

Selectable Text with Custom Context Menu

When users select text, a context menu appears. You can customize this menu with localized actions.

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

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

    return SelectableText(
      l10n.documentContent,
      style: Theme.of(context).textTheme.bodyLarge,
      contextMenuBuilder: (context, editableTextState) {
        final anchors = editableTextState.contextMenuAnchors;
        return AdaptiveTextSelectionToolbar.buttonItems(
          anchors: anchors,
          buttonItems: [
            ContextMenuButtonItem(
              label: l10n.copyAction,
              onPressed: () {
                editableTextState
                    .copySelection(SelectionChangedCause.toolbar);
              },
            ),
            ContextMenuButtonItem(
              label: l10n.selectAllAction,
              onPressed: () {
                editableTextState
                    .selectAll(SelectionChangedCause.toolbar);
              },
            ),
            ContextMenuButtonItem(
              label: l10n.shareAction,
              onPressed: () {
                editableTextState
                    .copySelection(SelectionChangedCause.toolbar);
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

RTL Support and Bidirectional Layouts

SelectableText automatically renders in the correct direction based on the ambient Directionality. Selection handles position themselves correctly for both LTR and RTL text.

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

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

    return Padding(
      padding: const EdgeInsetsDirectional.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            padding: const EdgeInsetsDirectional.only(start: 12),
            decoration: BoxDecoration(
              border: BorderDirectional(
                start: BorderSide(
                  color: Theme.of(context).colorScheme.primary,
                  width: 3,
                ),
              ),
            ),
            child: SelectableText(
              l10n.blockquoteText,
              style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                fontStyle: FontStyle.italic,
                height: 1.6,
              ),
              textAlign: TextAlign.start,
            ),
          ),
          const SizedBox(height: 8),
          SelectableText(
            l10n.blockquoteAttribution,
            style: Theme.of(context).textTheme.bodySmall,
            textAlign: TextAlign.end,
          ),
          const SizedBox(height: 24),
          SelectableText(
            l10n.copyrightNotice,
            style: Theme.of(context).textTheme.bodySmall?.copyWith(
              color: Theme.of(context).colorScheme.onSurfaceVariant,
            ),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

Testing SelectableText 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 Scaffold(body: LocalizedSelectableTextExample()),
    );
  }

  testWidgets('SelectableText renders in RTL for Arabic', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();

    final selectableText = tester.widget<SelectableText>(
      find.byType(SelectableText).first,
    );
    expect(selectableText, isNotNull);
  });

  testWidgets('SelectableText allows text selection', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();

    final selectableTextFinder = find.byType(SelectableText).first;
    expect(selectableTextFinder, findsOneWidget);

    await tester.longPress(selectableTextFinder);
    await tester.pumpAndSettle();
  });
}

Best Practices

  1. Use SelectableText for reference content like order IDs, tracking numbers, addresses, and legal text that users may need to copy.

  2. Use SelectableText.rich for mixed-style selectable content instead of placing multiple SelectableText widgets adjacent to each other.

  3. Adjust line height per script to ensure text selection handles don't overlap with tall scripts like Arabic, Thai, or Devanagari.

  4. Customize context menus with localized labels using contextMenuBuilder to provide translated Copy, Select All, and Share actions.

  5. Wrap long selectable content in SingleChildScrollView so users can scroll through and select portions of lengthy translated text.

  6. Test selection in RTL locales to verify that selection handles and highlight direction work correctly for Arabic and Hebrew.

Conclusion

SelectableText extends the Text widget with selection and copy capabilities, making it essential for multilingual content that users need to reference or share. By handling locale-aware styling, bidirectional selection, and custom context menus with translated labels, you can ensure SelectableText provides a polished experience across all supported languages.

Further Reading