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
Set
errorMaxLinesandhelperMaxLinesto 2 or more in your InputDecorationTheme so longer translated text wraps instead of truncating.Use
alignLabelWithHint: truefor multiline fields so the floating label aligns with the first line regardless of language.Swap
prefixTextandsuffixTextfor currency symbols based on RTL directionality -- some currencies appear after the amount in RTL locales.Define an
InputDecorationThemein your app theme for consistent padding, borders, and error styling across all localized fields.Use
EdgeInsetsDirectionalforcontentPaddingto ensure consistent insets in both LTR and RTL layouts.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.