Flutter CupertinoFormSection Localization: iOS Form Sections for Multilingual Apps
CupertinoFormSection is a Flutter widget that renders an iOS-style grouped form section with an optional header, footer, and a list of form rows. In multilingual applications, CupertinoFormSection is essential for displaying translated section headers and footers that guide users through form fields, grouping localized form inputs into logical iOS-style sections, supporting RTL text alignment for section titles and descriptions in Arabic and Hebrew, and building accessible form structures with section announcements in the active language.
Understanding CupertinoFormSection in Localization Context
CupertinoFormSection groups related form fields into visually distinct iOS-style sections with rounded corners and inset styling. For multilingual apps, this enables:
- Translated section headers (Personal Info, Payment Details, Preferences) above each group
- Localized footer text providing instructions or validation hints below sections
- RTL-compatible header and footer alignment for right-to-left languages
- Accessible section labels announced by VoiceOver in the active language
Why CupertinoFormSection Matters for Multilingual Apps
CupertinoFormSection provides:
- iOS consistency: Form grouping that matches native iOS Settings and form patterns in every language
- Contextual guidance: Translated headers and footers help users understand each form section
- Visual separation: Clear boundaries between localized form groups improve readability
- Platform feel: iOS users expect grouped form sections regardless of language
Basic CupertinoFormSection Implementation
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedCupertinoFormSectionExample extends StatelessWidget {
const LocalizedCupertinoFormSectionExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.profileSettingsTitle),
),
child: SafeArea(
child: ListView(
children: [
CupertinoFormSection.insetGrouped(
header: Text(l10n.personalInfoHeader),
footer: Text(l10n.personalInfoFooter),
children: [
CupertinoTextFormFieldRow(
prefix: Text(l10n.firstNameLabel),
placeholder: l10n.firstNamePlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.lastNameLabel),
placeholder: l10n.lastNamePlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.emailLabel),
placeholder: l10n.emailPlaceholder,
keyboardType: TextInputType.emailAddress,
),
],
),
CupertinoFormSection.insetGrouped(
header: Text(l10n.preferencesHeader),
children: [
CupertinoFormRow(
prefix: Text(l10n.notificationsLabel),
child: CupertinoSwitch(
value: true,
onChanged: (bool value) {},
),
),
CupertinoFormRow(
prefix: Text(l10n.darkModeLabel),
child: CupertinoSwitch(
value: false,
onChanged: (bool value) {},
),
),
],
),
],
),
),
);
}
}
Advanced CupertinoFormSection Patterns for Localization
Registration Form with Validation
CupertinoFormSection for a registration form with localized validation messages.
class RegistrationFormExample extends StatefulWidget {
const RegistrationFormExample({super.key});
@override
State<RegistrationFormExample> createState() =>
_RegistrationFormExampleState();
}
class _RegistrationFormExampleState extends State<RegistrationFormExample> {
final _formKey = GlobalKey<FormState>();
bool _agreeToTerms = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.registrationTitle),
),
child: SafeArea(
child: Form(
key: _formKey,
child: ListView(
children: [
CupertinoFormSection.insetGrouped(
header: Text(l10n.accountInfoHeader),
footer: Text(l10n.accountInfoFooter),
children: [
CupertinoTextFormFieldRow(
prefix: Text(l10n.usernameLabel),
placeholder: l10n.usernamePlaceholder,
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.usernameRequired;
}
return null;
},
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.emailLabel),
placeholder: l10n.emailPlaceholder,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !value.contains('@')) {
return l10n.emailInvalid;
}
return null;
},
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.passwordLabel),
placeholder: l10n.passwordPlaceholder,
obscureText: true,
validator: (value) {
if (value == null || value.length < 8) {
return l10n.passwordTooShort;
}
return null;
},
),
],
),
CupertinoFormSection.insetGrouped(
header: Text(l10n.personalDetailsHeader),
children: [
CupertinoTextFormFieldRow(
prefix: Text(l10n.fullNameLabel),
placeholder: l10n.fullNamePlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.phoneLabel),
placeholder: l10n.phonePlaceholder,
keyboardType: TextInputType.phone,
),
],
),
CupertinoFormSection.insetGrouped(
header: Text(l10n.legalHeader),
children: [
CupertinoFormRow(
prefix: Text(l10n.agreeToTermsLabel),
child: CupertinoSwitch(
value: _agreeToTerms,
onChanged: (bool value) {
setState(() {
_agreeToTerms = value;
});
},
),
),
],
),
Padding(
padding: const EdgeInsets.all(16),
child: CupertinoButton.filled(
onPressed: _agreeToTerms
? () {
if (_formKey.currentState!.validate()) {
// Submit registration
}
}
: null,
child: Text(l10n.registerButton),
),
),
],
),
),
),
);
}
}
Settings Page with Multiple Sections
CupertinoFormSection used to build an iOS-style settings page with translated labels.
class SettingsPageExample extends StatefulWidget {
const SettingsPageExample({super.key});
@override
State<SettingsPageExample> createState() => _SettingsPageExampleState();
}
class _SettingsPageExampleState extends State<SettingsPageExample> {
bool _pushNotifications = true;
bool _emailNotifications = false;
bool _locationServices = true;
bool _analytics = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.settingsTitle),
),
child: SafeArea(
child: ListView(
children: [
CupertinoFormSection.insetGrouped(
header: Text(l10n.notificationSettingsHeader),
footer: Text(l10n.notificationSettingsFooter),
children: [
CupertinoFormRow(
prefix: Text(l10n.pushNotificationsLabel),
child: CupertinoSwitch(
value: _pushNotifications,
onChanged: (bool value) {
setState(() => _pushNotifications = value);
},
),
),
CupertinoFormRow(
prefix: Text(l10n.emailNotificationsLabel),
child: CupertinoSwitch(
value: _emailNotifications,
onChanged: (bool value) {
setState(() => _emailNotifications = value);
},
),
),
],
),
CupertinoFormSection.insetGrouped(
header: Text(l10n.privacyHeader),
footer: Text(l10n.privacyFooter),
children: [
CupertinoFormRow(
prefix: Text(l10n.locationServicesLabel),
child: CupertinoSwitch(
value: _locationServices,
onChanged: (bool value) {
setState(() => _locationServices = value);
},
),
),
CupertinoFormRow(
prefix: Text(l10n.analyticsLabel),
child: CupertinoSwitch(
value: _analytics,
onChanged: (bool value) {
setState(() => _analytics = value);
},
),
),
],
),
CupertinoFormSection.insetGrouped(
header: Text(l10n.accountHeader),
children: [
CupertinoFormRow(
prefix: Text(l10n.changePasswordLabel),
child: const CupertinoListTileChevron(),
),
CupertinoFormRow(
prefix: Text(l10n.exportDataLabel),
child: const CupertinoListTileChevron(),
),
CupertinoFormRow(
prefix: Text(
l10n.deleteAccountLabel,
style: const TextStyle(
color: CupertinoColors.destructiveRed,
),
),
child: const CupertinoListTileChevron(),
),
],
),
],
),
),
);
}
}
Payment Form with Grouped Sections
CupertinoFormSection for a payment form with localized field groups.
class PaymentFormExample extends StatelessWidget {
const PaymentFormExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.paymentTitle),
),
child: SafeArea(
child: ListView(
children: [
CupertinoFormSection.insetGrouped(
header: Text(l10n.cardDetailsHeader),
footer: Text(l10n.cardDetailsFooter),
children: [
CupertinoTextFormFieldRow(
prefix: Text(l10n.cardNumberLabel),
placeholder: l10n.cardNumberPlaceholder,
keyboardType: TextInputType.number,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.cardholderNameLabel),
placeholder: l10n.cardholderNamePlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.expiryDateLabel),
placeholder: l10n.expiryDatePlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.cvvLabel),
placeholder: l10n.cvvPlaceholder,
obscureText: true,
keyboardType: TextInputType.number,
),
],
),
CupertinoFormSection.insetGrouped(
header: Text(l10n.billingAddressHeader),
children: [
CupertinoTextFormFieldRow(
prefix: Text(l10n.streetLabel),
placeholder: l10n.streetPlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.cityLabel),
placeholder: l10n.cityPlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.postalCodeLabel),
placeholder: l10n.postalCodePlaceholder,
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.countryLabel),
placeholder: l10n.countryPlaceholder,
),
],
),
Padding(
padding: const EdgeInsets.all(16),
child: CupertinoButton.filled(
onPressed: () {},
child: Text(l10n.payNowButton),
),
),
],
),
),
);
}
}
RTL Support and Bidirectional Layouts
CupertinoFormSection aligns its header and footer text according to the active text direction. Form rows within each section also respect RTL, placing prefix labels on the correct side.
class BidirectionalFormSectionExample extends StatelessWidget {
const BidirectionalFormSectionExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.profileSettingsTitle),
),
child: SafeArea(
child: ListView(
children: [
CupertinoFormSection.insetGrouped(
header: Text(l10n.personalInfoHeader),
footer: Text(l10n.personalInfoFooter),
children: [
CupertinoTextFormFieldRow(
prefix: Text(l10n.firstNameLabel),
placeholder: l10n.firstNamePlaceholder,
textDirection: Directionality.of(context),
),
CupertinoTextFormFieldRow(
prefix: Text(l10n.lastNameLabel),
placeholder: l10n.lastNamePlaceholder,
textDirection: Directionality.of(context),
),
],
),
],
),
),
);
}
}
Testing CupertinoFormSection 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 LocalizedCupertinoFormSectionExample(),
);
}
testWidgets('CupertinoFormSection renders with headers', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(CupertinoFormSection), findsWidgets);
});
testWidgets('CupertinoFormSection works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('CupertinoFormSection renders in Japanese', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ja')));
await tester.pumpAndSettle();
expect(find.byType(CupertinoFormSection), findsWidgets);
});
}
Best Practices
Use
insetGroupedconstructor for modern iOS-style rounded sections with proper inset margins, matching iOS 15+ design patterns across languages.Keep headers concise — Section headers should be 1-3 words in all supported languages to maintain visual consistency with iOS Settings conventions.
Provide footer hints — Use translated footer text to explain form requirements or provide validation hints beneath each section.
Group related fields — Organize localized form fields into logical sections (Personal Info, Payment, Preferences) so the structure makes sense in every language.
Test with verbose locales — German and Finnish section headers can be significantly longer, so verify they wrap correctly without breaking the layout.
Combine with CupertinoTextFormFieldRow for text inputs and CupertinoFormRow for switches and other controls to maintain consistent iOS form styling.
Conclusion
CupertinoFormSection provides iOS-style grouped form sections for Flutter apps. For multilingual apps, it handles translated section headers and footers, groups localized form fields into visually distinct sections, and supports RTL text alignment. By using the insetGrouped constructor, providing translated footer hints, and testing with verbose locales, you can build form layouts that match native iOS patterns in every supported language.