How to Build a Multi-Language Flutter App: Step-by-Step Tutorial
Building a Flutter app that supports multiple languages opens your app to billions of potential users worldwide. This comprehensive tutorial walks you through creating a fully functional multi-language Flutter app from scratch.
What We'll Build
By the end of this tutorial, you'll have a Flutter app that:
- Supports English, Spanish, French, and Arabic
- Automatically detects the user's preferred language
- Allows manual language switching
- Persists language preference
- Handles RTL layouts for Arabic
- Formats dates and numbers per locale
Prerequisites
- Flutter SDK installed (3.0+)
- Basic Flutter knowledge
- 30 minutes of your time
Step 1: Create a New Flutter Project
flutter create multilingual_app
cd multilingual_app
Step 2: Add Dependencies
Update your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
shared_preferences: ^2.2.2
flutter:
generate: true
uses-material-design: true
Run:
flutter pub get
Step 3: Configure Localization
Create l10n.yaml in your project root:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
Step 4: Create Translation Files
Create the lib/l10n/ directory and add your ARB files:
English (lib/l10n/app_en.arb)
{
"@@locale": "en",
"appTitle": "My Multilingual App",
"@appTitle": {
"description": "The title of the application"
},
"welcomeMessage": "Welcome!",
"@welcomeMessage": {
"description": "Welcome message on home screen"
},
"selectLanguage": "Select Language",
"@selectLanguage": {
"description": "Language selection header"
},
"currentLanguage": "Current language: {language}",
"@currentLanguage": {
"description": "Shows current language",
"placeholders": {
"language": {
"type": "String",
"example": "English"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Item count with pluralization",
"placeholders": {
"count": {
"type": "int"
}
}
},
"lastUpdated": "Last updated: {date}",
"@lastUpdated": {
"description": "Shows last update date",
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
"english": "English",
"spanish": "Spanish",
"french": "French",
"arabic": "Arabic",
"settings": "Settings",
"home": "Home",
"profile": "Profile",
"greeting": "Hello, {name}!",
"@greeting": {
"placeholders": {
"name": {
"type": "String"
}
}
}
}
Spanish (lib/l10n/app_es.arb)
{
"@@locale": "es",
"appTitle": "Mi App Multilingüe",
"welcomeMessage": "¡Bienvenido!",
"selectLanguage": "Seleccionar Idioma",
"currentLanguage": "Idioma actual: {language}",
"itemCount": "{count, plural, =0{Sin elementos} =1{1 elemento} other{{count} elementos}}",
"lastUpdated": "Última actualización: {date}",
"english": "Inglés",
"spanish": "Español",
"french": "Francés",
"arabic": "Árabe",
"settings": "Configuración",
"home": "Inicio",
"profile": "Perfil",
"greeting": "¡Hola, {name}!"
}
French (lib/l10n/app_fr.arb)
{
"@@locale": "fr",
"appTitle": "Mon App Multilingue",
"welcomeMessage": "Bienvenue!",
"selectLanguage": "Choisir la Langue",
"currentLanguage": "Langue actuelle: {language}",
"itemCount": "{count, plural, =0{Aucun élément} =1{1 élément} other{{count} éléments}}",
"lastUpdated": "Dernière mise à jour: {date}",
"english": "Anglais",
"spanish": "Espagnol",
"french": "Français",
"arabic": "Arabe",
"settings": "Paramètres",
"home": "Accueil",
"profile": "Profil",
"greeting": "Bonjour, {name}!"
}
Arabic (lib/l10n/app_ar.arb)
{
"@@locale": "ar",
"appTitle": "تطبيقي متعدد اللغات",
"welcomeMessage": "!مرحبًا",
"selectLanguage": "اختر اللغة",
"currentLanguage": "اللغة الحالية: {language}",
"itemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{{count} عناصر} many{{count} عنصرًا} other{{count} عنصر}}",
"lastUpdated": "آخر تحديث: {date}",
"english": "الإنجليزية",
"spanish": "الإسبانية",
"french": "الفرنسية",
"arabic": "العربية",
"settings": "الإعدادات",
"home": "الرئيسية",
"profile": "الملف الشخصي",
"greeting": "مرحبًا، {name}!"
}
Step 5: Generate Localization Files
flutter gen-l10n
This generates the AppLocalizations class in .dart_tool/flutter_gen/gen_l10n/.
Step 6: Create a Locale Provider
Create lib/providers/locale_provider.dart:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('en');
Locale get locale => _locale;
static const String _localeKey = 'selected_locale';
LocaleProvider() {
_loadSavedLocale();
}
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final savedLocale = prefs.getString(_localeKey);
if (savedLocale != null) {
_locale = Locale(savedLocale);
notifyListeners();
}
}
Future<void> setLocale(Locale locale) async {
if (_locale == locale) return;
_locale = locale;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, locale.languageCode);
}
static const List<Locale> supportedLocales = [
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('ar'),
];
String getLanguageName(String code) {
switch (code) {
case 'en': return 'English';
case 'es': return 'Español';
case 'fr': return 'Français';
case 'ar': return 'العربية';
default: return code;
}
}
}
Step 7: Update main.dart
Replace lib/main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'providers/locale_provider.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final LocaleProvider _localeProvider = LocaleProvider();
@override
void initState() {
super.initState();
_localeProvider.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Multilingual App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
// Localization configuration
locale: _localeProvider.locale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: LocaleProvider.supportedLocales,
home: HomeScreen(localeProvider: _localeProvider),
);
}
}
Step 8: Create the Home Screen
Create lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/locale_provider.dart';
import 'language_selector_screen.dart';
class HomeScreen extends StatefulWidget {
final LocaleProvider localeProvider;
const HomeScreen({super.key, required this.localeProvider});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
int _itemCount = 5;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.language),
onPressed: () => _openLanguageSelector(context),
),
],
),
body: _buildBody(context, l10n),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: l10n.home,
),
NavigationDestination(
icon: const Icon(Icons.person_outlined),
selectedIcon: const Icon(Icons.person),
label: l10n.profile,
),
NavigationDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: l10n.settings,
),
],
),
);
}
Widget _buildBody(BuildContext context, AppLocalizations l10n) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Welcome Card
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
l10n.welcomeMessage,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
l10n.greeting('Flutter Developer'),
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
),
const SizedBox(height: 20),
// Current Language Display
Card(
child: ListTile(
leading: const Icon(Icons.language),
title: Text(l10n.currentLanguage(
widget.localeProvider.getLanguageName(
widget.localeProvider.locale.languageCode,
),
)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openLanguageSelector(context),
),
),
const SizedBox(height: 20),
// Item Count Demo (Pluralization)
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pluralization Demo',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Text(
l10n.itemCount(_itemCount),
style: Theme.of(context).textTheme.bodyLarge,
),
Slider(
value: _itemCount.toDouble(),
min: 0,
max: 100,
divisions: 100,
label: _itemCount.toString(),
onChanged: (value) {
setState(() {
_itemCount = value.toInt();
});
},
),
],
),
),
),
const SizedBox(height: 20),
// Date Formatting Demo
Card(
child: ListTile(
leading: const Icon(Icons.calendar_today),
title: Text(l10n.lastUpdated(DateTime.now())),
),
),
],
),
);
}
void _openLanguageSelector(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LanguageSelectorScreen(
localeProvider: widget.localeProvider,
),
),
);
}
}
Step 9: Create the Language Selector Screen
Create lib/screens/language_selector_screen.dart:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/locale_provider.dart';
class LanguageSelectorScreen extends StatelessWidget {
final LocaleProvider localeProvider;
const LanguageSelectorScreen({super.key, required this.localeProvider});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(l10n.selectLanguage),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
children: [
_buildLanguageTile(
context,
locale: const Locale('en'),
name: l10n.english,
nativeName: 'English',
flag: '🇺🇸',
),
_buildLanguageTile(
context,
locale: const Locale('es'),
name: l10n.spanish,
nativeName: 'Español',
flag: '🇪🇸',
),
_buildLanguageTile(
context,
locale: const Locale('fr'),
name: l10n.french,
nativeName: 'Français',
flag: '🇫🇷',
),
_buildLanguageTile(
context,
locale: const Locale('ar'),
name: l10n.arabic,
nativeName: 'العربية',
flag: '🇸🇦',
),
],
),
);
}
Widget _buildLanguageTile(
BuildContext context, {
required Locale locale,
required String name,
required String nativeName,
required String flag,
}) {
final isSelected = localeProvider.locale == locale;
return ListTile(
leading: Text(flag, style: const TextStyle(fontSize: 24)),
title: Text(nativeName),
subtitle: Text(name),
trailing: isSelected
? Icon(Icons.check_circle, color: Theme.of(context).primaryColor)
: null,
onTap: () {
localeProvider.setLocale(locale);
Navigator.pop(context);
},
);
}
}
Step 10: Run Your App
flutter run
You now have a fully functional multi-language Flutter app!
Testing Your Multi-Language App
// test/widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
testWidgets('App displays Spanish when locale is es', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('es'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [Locale('en'), Locale('es')],
home: Builder(
builder: (context) {
final l10n = AppLocalizations.of(context);
return Text(l10n.welcomeMessage);
},
),
),
);
expect(find.text('¡Bienvenido!'), findsOneWidget);
});
}
Managing Translations at Scale
As your app grows to support more languages, manually managing ARB files becomes challenging. Common pain points include:
- Keeping translations in sync across 10+ languages
- Ensuring all keys are translated
- Managing translator access without Git knowledge
- Handling pluralization rules for complex languages
FlutterLocalisation: The Professional Solution
FlutterLocalisation eliminates these pain points with:
- Visual Translation Editor: Translators edit in a clean UI, not JSON files
- AI-Powered Translations: One-click translation to 50+ languages
- Automatic ARB Sync: Changes automatically sync to your Git repository
- Team Collaboration: Role-based access for developers, translators, and reviewers
- Missing Key Detection: Instant alerts for untranslated strings
Conclusion
You've built a complete multi-language Flutter app with:
- Four supported languages including RTL
- Persistent language preferences
- Pluralization support
- Locale-aware date formatting
- Clean architecture with provider pattern
This foundation scales to support dozens of languages. For production apps targeting global markets, consider using FlutterLocalisation to automate translation management.
Ready to scale your multi-language app? Try FlutterLocalisation free and manage translations across unlimited languages with AI-powered automation and Git integration.