Flutter Settings Screen Localization: Preferences and App Configuration
Build settings screens that feel native in every language. This guide covers localizing preferences, toggles, selections, and configuration options in Flutter.
Settings Localization Challenges
Settings screens require localization for:
- Section headers - Account, Notifications, Privacy
- Toggle labels - Enable/disable descriptions
- Selection options - Language, theme, units
- Confirmations - Destructive action warnings
- Help text - Feature explanations
Complete Settings Screen
Localized Settings Structure
class LocalizedSettingsScreen extends StatefulWidget {
@override
State<LocalizedSettingsScreen> createState() => _LocalizedSettingsScreenState();
}
class _LocalizedSettingsScreenState extends State<LocalizedSettingsScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings),
),
body: ListView(
children: [
// Account section
_buildSectionHeader(l10n.settingsAccount),
_buildAccountTile(l10n),
_buildNavigationTile(
icon: Icons.person,
title: l10n.editProfile,
onTap: () => _navigateTo('/profile/edit'),
),
_buildNavigationTile(
icon: Icons.security,
title: l10n.securitySettings,
onTap: () => _navigateTo('/settings/security'),
),
// Preferences section
_buildSectionHeader(l10n.settingsPreferences),
_buildLanguageTile(l10n),
_buildThemeTile(l10n),
_buildUnitsTile(l10n),
// Notifications section
_buildSectionHeader(l10n.settingsNotifications),
_buildNotificationToggles(l10n),
// Privacy section
_buildSectionHeader(l10n.settingsPrivacy),
_buildPrivacyToggles(l10n),
// Data section
_buildSectionHeader(l10n.settingsData),
_buildDataManagement(l10n),
// About section
_buildSectionHeader(l10n.settingsAbout),
_buildAboutTiles(l10n),
// Danger zone
_buildDangerZone(l10n),
SizedBox(height: 32),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title.toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
letterSpacing: 1.2,
),
),
);
}
Widget _buildAccountTile(AppLocalizations l10n) {
final user = context.watch<AuthProvider>().user;
return ListTile(
leading: CircleAvatar(
backgroundImage: user?.photoUrl != null
? NetworkImage(user!.photoUrl!)
: null,
child: user?.photoUrl == null
? Icon(Icons.person)
: null,
),
title: Text(user?.displayName ?? l10n.guest),
subtitle: Text(user?.email ?? l10n.notSignedIn),
trailing: Icon(Icons.chevron_right),
onTap: () => _navigateTo('/account'),
);
}
Widget _buildLanguageTile(AppLocalizations l10n) {
final currentLocale = Localizations.localeOf(context);
return ListTile(
leading: Icon(Icons.language),
title: Text(l10n.language),
subtitle: Text(_getLanguageName(currentLocale, l10n)),
trailing: Icon(Icons.chevron_right),
onTap: () => _showLanguagePicker(l10n),
);
}
String _getLanguageName(Locale locale, AppLocalizations l10n) {
final names = {
'en': l10n.languageEnglish,
'es': l10n.languageSpanish,
'fr': l10n.languageFrench,
'de': l10n.languageGerman,
'ja': l10n.languageJapanese,
'zh': l10n.languageChinese,
'ar': l10n.languageArabic,
'pt': l10n.languagePortuguese,
};
return names[locale.languageCode] ?? locale.languageCode;
}
void _showLanguagePicker(AppLocalizations l10n) {
final currentLocale = Localizations.localeOf(context);
showModalBottomSheet(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
l10n.selectLanguage,
style: Theme.of(context).textTheme.titleLarge,
),
),
..._getSupportedLanguages(l10n).map((lang) {
final isSelected = currentLocale.languageCode == lang.code;
return ListTile(
leading: Text(
lang.flag,
style: TextStyle(fontSize: 24),
),
title: Text(lang.name),
subtitle: Text(lang.nativeName),
trailing: isSelected
? Icon(Icons.check, color: Theme.of(context).primaryColor)
: null,
onTap: () {
_changeLanguage(lang.code);
Navigator.pop(context);
},
);
}),
SizedBox(height: 16),
],
),
);
}
List<LanguageOption> _getSupportedLanguages(AppLocalizations l10n) {
return [
LanguageOption(
code: 'en',
name: l10n.languageEnglish,
nativeName: 'English',
flag: '🇺🇸',
),
LanguageOption(
code: 'es',
name: l10n.languageSpanish,
nativeName: 'Español',
flag: '🇪🇸',
),
LanguageOption(
code: 'fr',
name: l10n.languageFrench,
nativeName: 'Français',
flag: '🇫🇷',
),
LanguageOption(
code: 'de',
name: l10n.languageGerman,
nativeName: 'Deutsch',
flag: '🇩🇪',
),
LanguageOption(
code: 'ja',
name: l10n.languageJapanese,
nativeName: '日本語',
flag: '🇯🇵',
),
LanguageOption(
code: 'zh',
name: l10n.languageChinese,
nativeName: '中文',
flag: '🇨🇳',
),
LanguageOption(
code: 'ar',
name: l10n.languageArabic,
nativeName: 'العربية',
flag: '🇸🇦',
),
];
}
Widget _buildThemeTile(AppLocalizations l10n) {
final themeMode = context.watch<ThemeProvider>().themeMode;
return ListTile(
leading: Icon(Icons.palette),
title: Text(l10n.theme),
subtitle: Text(_getThemeName(themeMode, l10n)),
trailing: Icon(Icons.chevron_right),
onTap: () => _showThemePicker(l10n),
);
}
String _getThemeName(ThemeMode mode, AppLocalizations l10n) {
switch (mode) {
case ThemeMode.system:
return l10n.themeSystem;
case ThemeMode.light:
return l10n.themeLight;
case ThemeMode.dark:
return l10n.themeDark;
}
}
void _showThemePicker(AppLocalizations l10n) {
final currentTheme = context.read<ThemeProvider>().themeMode;
showModalBottomSheet(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
l10n.selectTheme,
style: Theme.of(context).textTheme.titleLarge,
),
),
RadioListTile<ThemeMode>(
value: ThemeMode.system,
groupValue: currentTheme,
title: Text(l10n.themeSystem),
subtitle: Text(l10n.themeSystemDescription),
onChanged: (value) {
_changeTheme(value!);
Navigator.pop(context);
},
),
RadioListTile<ThemeMode>(
value: ThemeMode.light,
groupValue: currentTheme,
title: Text(l10n.themeLight),
onChanged: (value) {
_changeTheme(value!);
Navigator.pop(context);
},
),
RadioListTile<ThemeMode>(
value: ThemeMode.dark,
groupValue: currentTheme,
title: Text(l10n.themeDark),
onChanged: (value) {
_changeTheme(value!);
Navigator.pop(context);
},
),
SizedBox(height: 16),
],
),
);
}
Widget _buildUnitsTile(AppLocalizations l10n) {
final units = context.watch<PreferencesProvider>().measurementUnit;
return ListTile(
leading: Icon(Icons.straighten),
title: Text(l10n.measurementUnits),
subtitle: Text(_getUnitName(units, l10n)),
trailing: Icon(Icons.chevron_right),
onTap: () => _showUnitsPicker(l10n),
);
}
String _getUnitName(MeasurementUnit unit, AppLocalizations l10n) {
switch (unit) {
case MeasurementUnit.metric:
return l10n.unitsMetric;
case MeasurementUnit.imperial:
return l10n.unitsImperial;
}
}
Widget _buildNotificationToggles(AppLocalizations l10n) {
final prefs = context.watch<PreferencesProvider>();
return Column(
children: [
SwitchListTile(
secondary: Icon(Icons.notifications),
title: Text(l10n.pushNotifications),
subtitle: Text(l10n.pushNotificationsDescription),
value: prefs.pushEnabled,
onChanged: (value) => _toggleNotifications(value, l10n),
),
if (prefs.pushEnabled) ...[
SwitchListTile(
secondary: SizedBox(width: 24),
title: Text(l10n.notifyMessages),
value: prefs.notifyMessages,
onChanged: (value) => prefs.setNotifyMessages(value),
),
SwitchListTile(
secondary: SizedBox(width: 24),
title: Text(l10n.notifyUpdates),
value: prefs.notifyUpdates,
onChanged: (value) => prefs.setNotifyUpdates(value),
),
SwitchListTile(
secondary: SizedBox(width: 24),
title: Text(l10n.notifyPromotions),
value: prefs.notifyPromotions,
onChanged: (value) => prefs.setNotifyPromotions(value),
),
],
SwitchListTile(
secondary: Icon(Icons.email),
title: Text(l10n.emailNotifications),
subtitle: Text(l10n.emailNotificationsDescription),
value: prefs.emailEnabled,
onChanged: (value) => prefs.setEmailEnabled(value),
),
],
);
}
Widget _buildPrivacyToggles(AppLocalizations l10n) {
final prefs = context.watch<PreferencesProvider>();
return Column(
children: [
SwitchListTile(
secondary: Icon(Icons.analytics),
title: Text(l10n.analytics),
subtitle: Text(l10n.analyticsDescription),
value: prefs.analyticsEnabled,
onChanged: (value) => prefs.setAnalyticsEnabled(value),
),
SwitchListTile(
secondary: Icon(Icons.bug_report),
title: Text(l10n.crashReports),
subtitle: Text(l10n.crashReportsDescription),
value: prefs.crashReportsEnabled,
onChanged: (value) => prefs.setCrashReportsEnabled(value),
),
ListTile(
leading: Icon(Icons.privacy_tip),
title: Text(l10n.privacyPolicy),
trailing: Icon(Icons.open_in_new),
onTap: () => _openPrivacyPolicy(),
),
],
);
}
Widget _buildDataManagement(AppLocalizations l10n) {
return Column(
children: [
ListTile(
leading: Icon(Icons.cloud_download),
title: Text(l10n.exportData),
subtitle: Text(l10n.exportDataDescription),
onTap: () => _exportData(l10n),
),
ListTile(
leading: Icon(Icons.cleaning_services),
title: Text(l10n.clearCache),
subtitle: Text(l10n.clearCacheDescription),
onTap: () => _clearCache(l10n),
),
],
);
}
Widget _buildAboutTiles(AppLocalizations l10n) {
return Column(
children: [
ListTile(
leading: Icon(Icons.info),
title: Text(l10n.appVersion),
subtitle: FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('${snapshot.data!.version} (${snapshot.data!.buildNumber})');
}
return Text('...');
},
),
),
ListTile(
leading: Icon(Icons.description),
title: Text(l10n.termsOfService),
trailing: Icon(Icons.open_in_new),
onTap: () => _openTerms(),
),
ListTile(
leading: Icon(Icons.gavel),
title: Text(l10n.licenses),
trailing: Icon(Icons.chevron_right),
onTap: () => _showLicenses(l10n),
),
ListTile(
leading: Icon(Icons.help),
title: Text(l10n.helpCenter),
trailing: Icon(Icons.open_in_new),
onTap: () => _openHelp(),
),
ListTile(
leading: Icon(Icons.feedback),
title: Text(l10n.sendFeedback),
onTap: () => _sendFeedback(l10n),
),
],
);
}
Widget _buildDangerZone(AppLocalizations l10n) {
return Column(
children: [
_buildSectionHeader(l10n.dangerZone),
ListTile(
leading: Icon(Icons.logout, color: Colors.orange),
title: Text(l10n.signOut),
onTap: () => _confirmSignOut(l10n),
),
ListTile(
leading: Icon(Icons.delete_forever, color: Colors.red),
title: Text(
l10n.deleteAccount,
style: TextStyle(color: Colors.red),
),
onTap: () => _confirmDeleteAccount(l10n),
),
],
);
}
Future<void> _confirmSignOut(AppLocalizations l10n) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.signOutTitle),
content: Text(l10n.signOutMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.signOut),
),
],
),
);
if (confirm == true) {
await context.read<AuthProvider>().signOut();
}
}
Future<void> _confirmDeleteAccount(AppLocalizations l10n) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.deleteAccountTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.deleteAccountWarning),
SizedBox(height: 16),
Text(
l10n.deleteAccountConsequences,
style: TextStyle(color: Colors.red),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(l10n.deleteAccount),
),
],
),
);
if (confirm == true) {
await context.read<AuthProvider>().deleteAccount();
}
}
}
class LanguageOption {
final String code;
final String name;
final String nativeName;
final String flag;
LanguageOption({
required this.code,
required this.name,
required this.nativeName,
required this.flag,
});
}
enum MeasurementUnit { metric, imperial }
ARB File Structure
{
"@@locale": "en",
"settings": "Settings",
"settingsAccount": "Account",
"guest": "Guest",
"notSignedIn": "Not signed in",
"editProfile": "Edit Profile",
"securitySettings": "Security",
"settingsPreferences": "Preferences",
"language": "Language",
"selectLanguage": "Select Language",
"languageEnglish": "English",
"languageSpanish": "Spanish",
"languageFrench": "French",
"languageGerman": "German",
"languageJapanese": "Japanese",
"languageChinese": "Chinese",
"languageArabic": "Arabic",
"languagePortuguese": "Portuguese",
"theme": "Theme",
"selectTheme": "Select Theme",
"themeSystem": "System Default",
"themeSystemDescription": "Follow device settings",
"themeLight": "Light",
"themeDark": "Dark",
"measurementUnits": "Measurement Units",
"unitsMetric": "Metric (km, kg)",
"unitsImperial": "Imperial (mi, lb)",
"settingsNotifications": "Notifications",
"pushNotifications": "Push Notifications",
"pushNotificationsDescription": "Receive alerts on your device",
"notifyMessages": "New Messages",
"notifyUpdates": "App Updates",
"notifyPromotions": "Promotions & Offers",
"emailNotifications": "Email Notifications",
"emailNotificationsDescription": "Receive updates via email",
"settingsPrivacy": "Privacy",
"analytics": "Usage Analytics",
"analyticsDescription": "Help us improve by sharing anonymous usage data",
"crashReports": "Crash Reports",
"crashReportsDescription": "Automatically send crash reports",
"privacyPolicy": "Privacy Policy",
"settingsData": "Data Management",
"exportData": "Export My Data",
"exportDataDescription": "Download a copy of your data",
"clearCache": "Clear Cache",
"clearCacheDescription": "Free up storage space",
"cacheCleared": "Cache cleared successfully",
"settingsAbout": "About",
"appVersion": "App Version",
"termsOfService": "Terms of Service",
"licenses": "Open Source Licenses",
"helpCenter": "Help Center",
"sendFeedback": "Send Feedback",
"dangerZone": "Danger Zone",
"signOut": "Sign Out",
"signOutTitle": "Sign Out?",
"signOutMessage": "Are you sure you want to sign out?",
"deleteAccount": "Delete Account",
"deleteAccountTitle": "Delete Account?",
"deleteAccountWarning": "This action cannot be undone. All your data will be permanently deleted.",
"deleteAccountConsequences": "You will lose all your saved items, preferences, and history.",
"cancel": "Cancel",
"save": "Save",
"done": "Done"
}
Conclusion
Settings screen localization requires:
- Clear section organization with translated headers
- Descriptive toggle labels with explanations
- Localized selection options for language, theme, units
- Warning dialogs for destructive actions
- Native language names in language picker
With proper localization, your settings screen will help users configure the app in their preferred language.