Shipping an app in a single language leaves massive segments of the global market completely untapped. By implementing internationalisation (i18n) and localisation (l10n), you ensure your user interface adapts naturally to a user's native tongue, vastly improving retention and accessibility. A fully localised app feels native to the user, whether they are reading English in London, Spanish in Madrid, or Arabic in Dubai.
This tutorial explores the official Flutter localisation workflow using the intl package and Application Resource Bundle (.arb) files. We will set up code generation to create strongly typed translation delegates, tackle complex string formats like plurals and placeholders, and handle Right-to-Left (RTL) text directions automatically. We will also cover how to format dates and numbers according to regional conventions.
Before writing any code, it is crucial to remember that adding new languages is not just a matter of swapping out vocabulary. You must design your layouts to accommodate varying text lengths and directional shifts. We will also build a mechanism to switch locales dynamically at runtime, ensuring users are not permanently locked into their device's default system language.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowAdding the Required Dependencies
Flutter provides built-in localisation support, but it requires a few extra packages to handle code generation and complex formatting. You need to add the flutter_localizations SDK package, the intl package, and enable the code generator in your pubspec.yaml file.
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.2 # Compatible with current flutter_localizations
flutter:
uses-material-design: true
# Enable the generation of AppLocalizations
generate: true
The generate: true flag is the linchpin of this entire workflow. It tells the Flutter tool to automatically read your translation files and generate the Dart classes needed to access them during the build process. Without this, you would have to manually run generation scripts every time you changed a string.
Configuring the l10n.yaml File
To tell Flutter where your translation files live and what the generated classes should be named, you must create a configuration file named l10n.yaml in the root directory of your project (alongside pubspec.yaml).
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
Here is what these properties dictate:
- arb-dir: The folder where you will store your
.arbfiles. - template-arb-file: The base language file. Flutter uses this file as the source of truth to generate the method names and parameters in the Dart code.
- output-localization-file: The name of the generated Dart file that will contain your
AppLocalizationsclass.
Writing .arb Translation Files
Application Resource Bundle (.arb) files are essentially JSON files with a specific structure. Create a new directory at lib/l10n/ and add your base template file, app_en.arb.
{
"@@locale": "en",
"appTitle": "My Awesome App",
"@appTitle": {
"description": "The title of the application"
},
"greeting": "Hello, World!",
"@greeting": {
"description": "A simple greeting message"
}
}
Every key-value pair represents a string in your app. The keys starting with @ are optional metadata blocks where you can provide descriptions or define placeholders. This metadata is invaluable for translators who need context to provide accurate translations.
Next, create a Spanish translation file named app_es.arb in the same folder:
{
"@@locale": "es",
"appTitle": "Mi Increíble Aplicación",
"greeting": "¡Hola, Mundo!"
}

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowInjecting AppLocalizations into MaterialApp
Once you have created the .arb files, run flutter gen-l10n or start a Flutter build. Current Flutter writes app_localizations.dart into the configured source directory (here, lib/l10n/); the old synthetic package:flutter_gen output was removed. Import the generated source file from your application instead:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
// main.dart is in lib/, so this resolves lib/l10n/app_localizations.dart.
import 'l10n/app_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Localised App',
// Provide the delegates to the app
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// Define the locales your app supports
supportedLocales: const [
Locale('en'), // English
Locale('es'), // Spanish
],
home: const HomePage(),
);
}
}
To display a translated string in your UI, read it from the AppLocalizations instance provided by the BuildContext:
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// Access the generated strings
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
),
body: Center(
child: Text(l10n.greeting),
),
);
}
}
Handling Placeholders, Plurals, and Selects
Static text is rarely enough for a dynamic application. You will often need to inject user names, handle pluralisation ("1 item" vs "2 items"), or select words based on gender. The intl package syntax handles this elegantly within your .arb files.
Update your app_en.arb to include these complex patterns:
{
"@@locale": "en",
"welcomeUser": "Welcome back, {userName}!",
"@welcomeUser": {
"placeholders": {
"userName": {
"type": "String"
}
}
},
"messageCount": "{count, plural, =0{No new messages} =1{You have 1 new message} other{You have {count} new messages}}",
"@messageCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pronoun": "{gender, select, male{He} female{She} other{They}} liked your post.",
"@pronoun": {
"placeholders": {
"gender": {
"type": "String"
}
}
}
}
When Flutter generates the Dart code for these entries, they become methods that accept arguments, ensuring type safety at compile time.
// Usage in your Dart code
Text(l10n.welcomeUser('Alice'));
Text(l10n.messageCount(5)); // Outputs: You have 5 new messages
Text(l10n.pronoun('female')); // Outputs: She liked your post.
Formatting Dates and Numbers
Different cultures format dates and numbers differently. For example, 1,000.50 in the UK is written as 1.000,50 in many European countries. The intl package provides DateFormat and NumberFormat classes that automatically adapt to the user's current locale.
import 'package:intl/intl.dart';
class FormattingExample extends StatelessWidget {
const FormattingExample({super.key});
@override
Widget build(BuildContext context) {
// Preserve region as well as language (for example, en_GB vs en_US).
final localeString = Localizations.localeOf(context).toString();
final DateTime now = DateTime.now();
final double price = 1234.56;
// Format date based on locale
final formattedDate = DateFormat.yMMMMd(localeString).format(now);
// Format currency based on locale
final formattedPrice = NumberFormat.simpleCurrency(
localeName: localeString,
name: 'EUR'
).format(price);
return Column(
children: [
Text('Date: $formattedDate'),
Text('Price: $formattedPrice'),
],
);
}
}
Passing the complete locale preserves regional differences such as en_GB versus en_US. Treat the result as a locale-aware default rather than an absolute guarantee: checkout flows should still use the currency and formatting rules required by the selected market.
Changing the Locale at Runtime
Relying solely on the device's system language is restrictive. Many users prefer their device in English but want a specific app in their native language. To allow dynamic language switching, you must wrap your MaterialApp in a state-holder that can trigger a rebuild with a new Locale.
Here is an approach using a ValueNotifier to manage the locale state globally for demonstration purposes:
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
// A global notifier to hold the current locale
final ValueNotifier<Locale> appLocaleNotifier = ValueNotifier(const Locale('en'));
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Locale>(
valueListenable: appLocaleNotifier,
builder: (context, currentLocale, child) {
return MaterialApp(
locale: currentLocale, // Inject the dynamic locale here
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LanguageSettingsScreen(),
);
},
);
}
}
class LanguageSettingsScreen extends StatelessWidget {
const LanguageSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => appLocaleNotifier.value = const Locale('en'),
child: const Text('English'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: () => appLocaleNotifier.value = const Locale('es'),
child: const Text('Español'),
),
],
),
),
);
}
}
In a production application, you would typically persist this preference using a package like shared_preferences so the app remembers the user's choice across restarts.
Supporting Right-to-Left (RTL) Languages
When you add support for an RTL language like Arabic (ar) or Hebrew (he), Flutter handles the text direction automatically. The entire UI will flip horizontally: standard Row widgets will lay out their children from right to left, and navigation transitions will reverse.
However, you must be careful with hardcoded directional alignments. If you use EdgeInsets.only(left: 16), that padding will remain on the physical left side of the screen, which is incorrect in RTL (it should be on the starting edge). Instead, you must use directional variants:
// BAD: Fixed to the physical left/right. Breaks RTL layouts.
Padding(
padding: EdgeInsets.only(left: 16.0, right: 8.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(l10n.greeting),
),
);
// GOOD: Adapts to text direction. Uses start/end instead of left/right.
Padding(
padding: EdgeInsetsDirectional.only(start: 16.0, end: 8.0),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.greeting),
),
);
Always audit your codebase for Positioned, EdgeInsets, and Alignment properties. Replace them with their Directional counterparts to ensure your interface mirrors flawlessly when an RTL locale is active.
Common Localisation Pitfalls
One of the most frequent errors developers encounter is a null check operator exception when calling AppLocalizations.of(context)!. This happens if you attempt to access the localisations before the MaterialApp is fully initialised, or from a context that sits above the MaterialApp in the widget tree. If you need a localised string for the MaterialApp.title property, use the onGenerateTitle callback instead:
MaterialApp(
// BAD: context here does not contain localisations yet
// title: AppLocalizations.of(context)!.appTitle,
// GOOD: context is provided by the builder after delegates load
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
)
Another common mistake is concatenating strings manually (e.g., 'Hello ' + userName). Different languages structure their grammar differently; the name might need to appear at the beginning or middle of the sentence. Always use .arb placeholders so translators can reposition the variables to fit the grammatical rules of their language.
Further reads
Keep going with these related tutorials from this site.
- Flutter: The Complete Guide — the full Flutter learning path on this site
- Flutter Flavors and Environment Config — how to manage different build configurations
- Flutter Build APK and App Bundle — steps to package your localised app for release
- Flutter Internationalisation (i18n) — review this guide if you need a refresher on intl
- Flutter Layout Widgets Guide — learn how directional widgets adapt to RTL text