← Back to Blog

Flutter CupertinoTextField Localization: iOS-Style Text Input for Multilingual Apps

fluttercupertinotextfieldioslocalizationrtl

Flutter CupertinoTextField Localization: iOS-Style Text Input for Multilingual Apps

CupertinoTextField is a Flutter widget that provides an iOS-style text input field with rounded borders, placeholder text, and clear button. In multilingual applications, CupertinoTextField is essential for building iOS-native forms with translated placeholders, handling locale-specific input formatting, providing localized prefix and suffix labels, and supporting RTL text entry with iOS styling conventions.

Understanding CupertinoTextField in Localization Context

CupertinoTextField renders an iOS-style input field with rounded rectangle borders, a placeholder that disappears on focus, and an optional clear button. For multilingual apps, this enables:

  • Translated placeholder text that shows locale-appropriate examples
  • Localized prefix and suffix labels for currency, units, and context
  • iOS-native clear button with translated accessibility labels
  • RTL text input with correctly positioned decorations

Why CupertinoTextField Matters for Multilingual Apps

CupertinoTextField provides:

  • iOS-native appearance: Rounded border styling with translated placeholders
  • Placeholder localization: iOS-style hint text that disappears on focus
  • Clear button: Built-in clear affordance with localized VoiceOver labels
  • Prefix/suffix: Locale-aware labels and icons positioned in iOS style

Basic CupertinoTextField Implementation

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

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

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.contactFormTitle),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              CupertinoTextField(
                placeholder: l10n.fullNamePlaceholder,
                prefix: const Padding(
                  padding: EdgeInsetsDirectional.only(start: 8),
                  child: Icon(CupertinoIcons.person),
                ),
                clearButtonMode: OverlayVisibilityMode.editing,
                textCapitalization: TextCapitalization.words,
              ),
              const SizedBox(height: 12),
              CupertinoTextField(
                placeholder: l10n.emailPlaceholder,
                prefix: const Padding(
                  padding: EdgeInsetsDirectional.only(start: 8),
                  child: Icon(CupertinoIcons.mail),
                ),
                clearButtonMode: OverlayVisibilityMode.editing,
                keyboardType: TextInputType.emailAddress,
              ),
              const SizedBox(height: 12),
              CupertinoTextField(
                placeholder: l10n.phonePlaceholder,
                prefix: const Padding(
                  padding: EdgeInsetsDirectional.only(start: 8),
                  child: Icon(CupertinoIcons.phone),
                ),
                clearButtonMode: OverlayVisibilityMode.editing,
                keyboardType: TextInputType.phone,
              ),
              const SizedBox(height: 12),
              CupertinoTextField(
                placeholder: l10n.messagePlaceholder,
                maxLines: 4,
                textAlignVertical: TextAlignVertical.top,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Advanced CupertinoTextField Patterns for Localization

iOS Settings-Style Form

iOS settings forms use grouped CupertinoTextFields within CupertinoListSection for a native look with translated labels.

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

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.accountSettingsTitle),
      ),
      child: SafeArea(
        child: ListView(
          children: [
            CupertinoListSection.insetGrouped(
              header: Text(l10n.personalInfoSection),
              children: [
                CupertinoListTile(
                  title: Text(l10n.firstNameLabel),
                  additionalInfo: SizedBox(
                    width: 180,
                    child: CupertinoTextField(
                      placeholder: l10n.firstNamePlaceholder,
                      decoration: null,
                      textAlign: TextAlign.end,
                    ),
                  ),
                ),
                CupertinoListTile(
                  title: Text(l10n.lastNameLabel),
                  additionalInfo: SizedBox(
                    width: 180,
                    child: CupertinoTextField(
                      placeholder: l10n.lastNamePlaceholder,
                      decoration: null,
                      textAlign: TextAlign.end,
                    ),
                  ),
                ),
                CupertinoListTile(
                  title: Text(l10n.emailLabel),
                  additionalInfo: SizedBox(
                    width: 200,
                    child: CupertinoTextField(
                      placeholder: l10n.emailPlaceholder,
                      decoration: null,
                      textAlign: TextAlign.end,
                      keyboardType: TextInputType.emailAddress,
                    ),
                  ),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              header: Text(l10n.securitySection),
              children: [
                CupertinoListTile(
                  title: Text(l10n.currentPasswordLabel),
                  additionalInfo: SizedBox(
                    width: 180,
                    child: CupertinoTextField(
                      placeholder: l10n.passwordPlaceholder,
                      decoration: null,
                      textAlign: TextAlign.end,
                      obscureText: true,
                    ),
                  ),
                ),
                CupertinoListTile(
                  title: Text(l10n.newPasswordLabel),
                  additionalInfo: SizedBox(
                    width: 180,
                    child: CupertinoTextField(
                      placeholder: l10n.newPasswordPlaceholder,
                      decoration: null,
                      textAlign: TextAlign.end,
                      obscureText: true,
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Search Field with Localized Placeholder

CupertinoSearchTextField provides an iOS-style search bar with a translated placeholder and cancel button.

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

  @override
  State<LocalizedCupertinoSearch> createState() =>
      _LocalizedCupertinoSearchState();
}

class _LocalizedCupertinoSearchState extends State<LocalizedCupertinoSearch> {
  String _searchText = '';

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

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(l10n.searchTitle),
      ),
      child: SafeArea(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(8),
              child: CupertinoSearchTextField(
                placeholder: l10n.searchPlaceholder,
                onChanged: (value) {
                  setState(() => _searchText = value);
                },
              ),
            ),
            Expanded(
              child: _searchText.isEmpty
                  ? Center(
                      child: Text(
                        l10n.searchPromptMessage,
                        style: const TextStyle(
                          color: CupertinoColors.systemGrey,
                        ),
                      ),
                    )
                  : ListView.builder(
                      itemCount: 10,
                      itemBuilder: (context, index) {
                        return CupertinoListTile(
                          title: Text(
                            '${l10n.resultLabel} ${index + 1}',
                          ),
                          subtitle: Text(l10n.resultDescription),
                          trailing: const CupertinoListTileChevron(),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

Currency and Number Input with Locale Formatting

CupertinoTextField with locale-aware formatting for currency amounts and numeric values.

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

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

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          CupertinoTextField(
            placeholder: l10n.pricePlaceholder,
            prefix: Padding(
              padding: const EdgeInsetsDirectional.only(start: 8),
              child: Text(
                l10n.currencySymbol,
                style: const TextStyle(
                  color: CupertinoColors.systemGrey,
                  fontSize: 16,
                ),
              ),
            ),
            keyboardType:
                const TextInputType.numberWithOptions(decimal: true),
            textDirection: TextDirection.ltr,
          ),
          const SizedBox(height: 12),
          CupertinoTextField(
            placeholder: l10n.quantityPlaceholder,
            suffix: Padding(
              padding: const EdgeInsetsDirectional.only(end: 8),
              child: Text(
                l10n.unitsLabel,
                style: const TextStyle(
                  color: CupertinoColors.systemGrey,
                  fontSize: 14,
                ),
              ),
            ),
            keyboardType: TextInputType.number,
          ),
          const SizedBox(height: 12),
          CupertinoTextField(
            placeholder: l10n.weightPlaceholder,
            suffix: Padding(
              padding: const EdgeInsetsDirectional.only(end: 8),
              child: Text(
                l10n.weightUnitLabel,
                style: const TextStyle(
                  color: CupertinoColors.systemGrey,
                  fontSize: 14,
                ),
              ),
            ),
            keyboardType:
                const TextInputType.numberWithOptions(decimal: true),
          ),
        ],
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

CupertinoTextField automatically handles RTL text input. The prefix moves to the trailing side, the clear button repositions, and placeholder text aligns according to the active directionality.

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

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

    return Padding(
      padding: const EdgeInsetsDirectional.all(16),
      child: Column(
        children: [
          CupertinoTextField(
            placeholder: l10n.searchPlaceholder,
            prefix: const Padding(
              padding: EdgeInsetsDirectional.only(start: 8),
              child: Icon(CupertinoIcons.search),
            ),
            clearButtonMode: OverlayVisibilityMode.editing,
            textAlign: TextAlign.start,
          ),
          const SizedBox(height: 12),
          CupertinoTextField(
            placeholder: l10n.notesPlaceholder,
            maxLines: 3,
            textAlignVertical: TextAlignVertical.top,
            textAlign: TextAlign.start,
          ),
        ],
      ),
    );
  }
}

Testing CupertinoTextField Localization

import 'package:flutter/cupertino.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 CupertinoApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedCupertinoTextFieldExample(),
    );
  }

  testWidgets('CupertinoTextField shows localized placeholder', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(CupertinoTextField), findsWidgets);
  });

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

Best Practices

  1. Use CupertinoSearchTextField for search bars instead of styling a regular CupertinoTextField -- it provides the built-in iOS search appearance with localized cancel button.

  2. Set clearButtonMode: OverlayVisibilityMode.editing so users can quickly clear translated input without selecting all text.

  3. Use CupertinoListSection.insetGrouped for settings-style forms where each CupertinoTextField appears inline with translated labels.

  4. Keep numeric fields textDirection: TextDirection.ltr even in RTL locales since numbers are universally left-to-right.

  5. Provide translated prefix and suffix widgets for units, currency symbols, and context that help users understand the expected input.

  6. Test placeholder text in verbose languages (German, Finnish) to verify it doesn't clip within the default CupertinoTextField height.

Conclusion

CupertinoTextField provides iOS-native text input styling for Flutter apps. For multilingual apps, it handles translated placeholders, localized prefix/suffix labels, and RTL text entry while maintaining the expected iOS rounded-rectangle appearance. By combining CupertinoTextField with settings-style forms, search bars, and locale-aware numeric formatting, you can build iOS text input experiences that feel native in every supported language.

Further Reading