← Back to Blog

Flutter InputDecoration Localization: Styling Text Fields for Multilingual Apps

flutterinputdecorationtextfieldformslocalizationrtl

Flutter InputDecoration Localization: Styling Text Fields for Multilingual Apps

InputDecoration is a Flutter class that defines the visual appearance and behavior of text fields, including labels, hints, helper text, error messages, icons, and borders. In multilingual applications, InputDecoration is critical for providing translated labels and hints that guide input, displaying localized error and helper text, adapting prefix and suffix content for different locales, and ensuring decoration elements align correctly in RTL layouts.

Understanding InputDecoration in Localization Context

InputDecoration configures the visual elements surrounding a TextField or TextFormField -- the floating label, hint text, helper text, error text, counter text, and prefix/suffix widgets. For multilingual apps, this enables:

  • Floating labels that animate smoothly regardless of translation length
  • Hint text that shows locale-appropriate placeholder examples
  • Error messages displayed inline in the active language
  • Helper text providing translated guidance before user interaction

Why InputDecoration Matters for Multilingual Apps

InputDecoration provides:

  • Label management: Floating labels that adapt to translated text length
  • Contextual guidance: Helper and hint text in the active language
  • Error display: Inline validation errors with localized messages
  • Prefix/suffix: Currency symbols, units, and icons that adapt to locale

Basic InputDecoration Implementation

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

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.shippingFormTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: l10n.recipientNameLabel,
                hintText: l10n.recipientNameHint,
                helperText: l10n.recipientNameHelper,
                prefixIcon: const Icon(Icons.person),
                border: const OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: l10n.streetAddressLabel,
                hintText: l10n.streetAddressHint,
                prefixIcon: const Icon(Icons.location_on),
                border: const OutlineInputBorder(),
              ),
              maxLines: 2,
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: l10n.postalCodeLabel,
                hintText: l10n.postalCodeHint,
                helperText: l10n.postalCodeHelper,
                prefixIcon: const Icon(Icons.markunread_mailbox),
                border: const OutlineInputBorder(),
              ),
              keyboardType: TextInputType.text,
            ),
          ],
        ),
      ),
    );
  }
}

Advanced InputDecoration Patterns for Localization

Currency Input with Locale-Aware Prefix

Currency fields need locale-specific currency symbols positioned correctly for LTR and RTL layouts.

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

  String _getCurrencySymbol(Locale locale) {
    switch (locale.countryCode) {
      case 'US':
        return '\$';
      case 'GB':
        return '£';
      case 'EU' || 'DE' || 'FR':
        return '€';
      case 'JP':
        return '¥';
      case 'SA' || 'AE':
        return 'ر.س';
      default:
        return '\$';
    }
  }

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

    return TextField(
      decoration: InputDecoration(
        labelText: l10n.amountLabel,
        hintText: l10n.amountHint,
        helperText: l10n.amountHelper,
        prefixText: isRtl ? null : '$currencySymbol ',
        suffixText: isRtl ? ' $currencySymbol' : null,
        prefixIcon: const Icon(Icons.payments),
        border: const OutlineInputBorder(),
      ),
      keyboardType: const TextInputType.numberWithOptions(decimal: true),
      textDirection: TextDirection.ltr,
    );
  }
}

InputDecoration Theme for Consistent Localized Fields

Define a reusable InputDecoration theme so all text fields share consistent translated styling.

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

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

    final inputTheme = InputDecorationTheme(
      border: const OutlineInputBorder(),
      floatingLabelBehavior: FloatingLabelBehavior.auto,
      contentPadding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 16),
      errorStyle: TextStyle(
        color: Theme.of(context).colorScheme.error,
        fontWeight: FontWeight.w500,
      ),
      helperMaxLines: 2,
      errorMaxLines: 2,
    );

    return Theme(
      data: Theme.of(context).copyWith(
        inputDecorationTheme: inputTheme,
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: l10n.usernameLabel,
                hintText: l10n.usernameHint,
                helperText: l10n.usernameHelper,
                prefixIcon: const Icon(Icons.alternate_email),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: l10n.websiteLabel,
                hintText: l10n.websiteHint,
                prefixIcon: const Icon(Icons.link),
                prefixText: 'https://',
              ),
              keyboardType: TextInputType.url,
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: l10n.bioLabel,
                hintText: l10n.bioHint,
                helperText: l10n.bioHelper,
                alignLabelWithHint: true,
                counterText: l10n.characterCountLabel(0, 200),
              ),
              maxLines: 4,
              maxLength: 200,
            ),
          ],
        ),
      ),
    );
  }
}

Error and Success States with Localized Feedback

InputDecoration supports error text for validation failures and can be combined with suffix icons for success states.

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

  @override
  State<LocalizedFieldStates> createState() => _LocalizedFieldStatesState();
}

class _LocalizedFieldStatesState extends State<LocalizedFieldStates> {
  String _email = '';
  bool _isChecking = false;
  bool? _isAvailable;

  Future<void> _checkAvailability(String email) async {
    if (email.isEmpty || !email.contains('@')) return;
    setState(() => _isChecking = true);

    await Future.delayed(const Duration(seconds: 1));

    if (mounted) {
      setState(() {
        _isChecking = false;
        _isAvailable = email.length % 2 == 0;
      });
    }
  }

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

    Widget? suffixIcon;
    String? errorText;
    String? helperText;

    if (_isChecking) {
      suffixIcon = const Padding(
        padding: EdgeInsets.all(12),
        child: SizedBox(
          width: 20,
          height: 20,
          child: CircularProgressIndicator(strokeWidth: 2),
        ),
      );
      helperText = l10n.checkingAvailability;
    } else if (_isAvailable == true) {
      suffixIcon = const Icon(Icons.check_circle, color: Colors.green);
      helperText = l10n.emailAvailable;
    } else if (_isAvailable == false) {
      suffixIcon = const Icon(Icons.cancel, color: Colors.red);
      errorText = l10n.emailAlreadyTaken;
    }

    return Padding(
      padding: const EdgeInsets.all(16),
      child: TextField(
        decoration: InputDecoration(
          labelText: l10n.emailLabel,
          hintText: l10n.emailHint,
          prefixIcon: const Icon(Icons.email),
          suffixIcon: suffixIcon,
          errorText: errorText,
          helperText: errorText == null ? helperText : null,
          border: const OutlineInputBorder(),
        ),
        keyboardType: TextInputType.emailAddress,
        onChanged: (value) {
          _email = value;
          _isAvailable = null;
          _checkAvailability(value);
        },
      ),
    );
  }
}

Grouped Fields with Shared Decoration Pattern

Related fields share a decoration factory that applies consistent translated styling.

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

  InputDecoration _buildDecoration({
    required String label,
    required String hint,
    required IconData icon,
    String? helper,
  }) {
    return InputDecoration(
      labelText: label,
      hintText: hint,
      helperText: helper,
      prefixIcon: Icon(icon),
      border: const OutlineInputBorder(),
      filled: true,
    );
  }

  @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.paymentDetailsSection,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 12),
          TextField(
            decoration: _buildDecoration(
              label: l10n.cardholderNameLabel,
              hint: l10n.cardholderNameHint,
              icon: Icons.person,
            ),
          ),
          const SizedBox(height: 12),
          TextField(
            decoration: _buildDecoration(
              label: l10n.cardNumberLabel,
              hint: l10n.cardNumberHint,
              icon: Icons.credit_card,
              helper: l10n.cardNumberHelper,
            ),
            keyboardType: TextInputType.number,
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: TextField(
                  decoration: _buildDecoration(
                    label: l10n.expiryLabel,
                    hint: l10n.expiryHint,
                    icon: Icons.calendar_today,
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: TextField(
                  decoration: _buildDecoration(
                    label: l10n.cvvLabel,
                    hint: l10n.cvvHint,
                    icon: Icons.security,
                    helper: l10n.cvvHelper,
                  ),
                  obscureText: true,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

InputDecoration elements automatically flip in RTL. Prefix icons move right, suffix icons move left, and text alignment follows the ambient directionality.

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

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

    return Padding(
      padding: const EdgeInsetsDirectional.all(16),
      child: Column(
        children: [
          TextField(
            decoration: InputDecoration(
              labelText: l10n.fullNameLabel,
              prefixIcon: const Icon(Icons.person),
              suffixIcon: const Icon(Icons.check),
              border: const OutlineInputBorder(),
              contentPadding:
                  const EdgeInsetsDirectional.fromSTEB(16, 12, 16, 12),
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            decoration: InputDecoration(
              labelText: l10n.phoneNumberLabel,
              prefixIcon: const Icon(Icons.phone),
              border: const OutlineInputBorder(),
              contentPadding:
                  const EdgeInsetsDirectional.fromSTEB(16, 12, 16, 12),
            ),
            textDirection: TextDirection.ltr,
            keyboardType: TextInputType.phone,
          ),
        ],
      ),
    );
  }
}

Testing InputDecoration 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 LocalizedInputDecorationExample(),
    );
  }

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

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

Best Practices

  1. Set errorMaxLines and helperMaxLines to 2 or more in your InputDecorationTheme so longer translated text wraps instead of truncating.

  2. Use alignLabelWithHint: true for multiline fields so the floating label aligns with the first line regardless of language.

  3. Swap prefixText and suffixText for currency symbols based on RTL directionality -- some currencies appear after the amount in RTL locales.

  4. Define an InputDecorationTheme in your app theme for consistent padding, borders, and error styling across all localized fields.

  5. Use EdgeInsetsDirectional for contentPadding to ensure consistent insets in both LTR and RTL layouts.

  6. Test with long translations (German, Finnish) to verify that floating labels, helper text, and error messages don't overflow or clip.

Conclusion

InputDecoration is the styling backbone of every text field in Flutter. For multilingual apps, it provides the visual structure for translated labels, hints, helper text, and error messages that guide users through form input. By configuring locale-aware currency prefixes, consistent decoration themes, error/success states with translated feedback, and RTL-adaptive content padding, you ensure that every text field communicates clearly and looks polished in every supported language.

Further Reading