Internationalization (i18n) in Flutter
By Ann Tech · 26 November 2025
Internationalization (i18n) means making your app work in multiple languages and locales. Flutter's built-in localization system, combined with the intl package, handles translations, plural forms, date/number formatting, and text directionality.
Setup
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
generate: true # Enable code generation for ARB files
Create l10n.yaml at the project root:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
ARB files
ARB (Application Resource Bundle) is Flutter's translation format:
// lib/l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The application title shown in the app bar"
},
"welcomeMessage": "Welcome, {name}!",
"@welcomeMessage": {
"description": "Welcome message on the home screen",
"placeholders": {
"name": {
"type": "String",
"example": "Alice"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Number of items in the cart",
"placeholders": {
"count": {
"type": "num",
"format": "compact"
}
}
},
"orderDate": "Ordered on {date}",
"@orderDate": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMMd"
}
}
}
}
// lib/l10n/app_fr.arb
{
"@@locale": "fr",
"appTitle": "Mon Application",
"welcomeMessage": "Bienvenue, {name} !",
"itemCount": "{count, plural, =0{Aucun article} =1{1 article} other{{count} articles}}",
"orderDate": "Commandé le {date}"
}
Run the generator:
flutter gen-l10n
Configuring MaterialApp
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
// Optional: override system locale
locale: userSelectedLocale,
)
Using translations in widgets
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
Text(l10n.welcomeMessage(user.name)),
Text(l10n.itemCount(cart.itemCount)),
Text(l10n.orderDate(order.date)),
],
),
);
}
}
Locale switching at runtime
Let users change language in settings:
final localeProvider = StateProvider<Locale?>((ref) => null);
// In MaterialApp
locale: ref.watch(localeProvider),
// Settings screen
class LanguageSelector extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return DropdownButton<Locale>(
value: ref.watch(localeProvider),
items: [
const DropdownMenuItem(value: Locale('en'), child: Text('English')),
const DropdownMenuItem(value: Locale('fr'), child: Text('Français')),
const DropdownMenuItem(value: Locale('de'), child: Text('Deutsch')),
const DropdownMenuItem(value: Locale('ar'), child: Text('عربي')),
],
onChanged: (locale) {
ref.read(localeProvider.notifier).state = locale;
// Persist to shared_preferences
},
);
}
}
Number and date formatting with intl
The generated code handles basic formatting, but for custom formats use intl directly:
import 'package:intl/intl.dart';
// Currency
final price = NumberFormat.currency(
locale: Localizations.localeOf(context).toString(),
symbol: '\$',
).format(order.totalAmount);
// Compact numbers
final followers = NumberFormat.compact().format(1200000); // "1.2M"
// Relative time
final daysAgo = DateFormat.yMMMd().format(date);
RTL support
Arabic, Hebrew, and Persian text is right-to-left. Flutter handles this automatically when GlobalWidgetsLocalizations.delegate is included and the locale is set correctly.
For custom widgets, use Directionality to detect and adapt:
final isRtl = Directionality.of(context) == TextDirection.rtl;
Row(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
children: [
const Icon(Icons.arrow_back),
const SizedBox(width: 8),
Text(l10n.backButton),
],
)
Or use EdgeInsetsDirectional instead of EdgeInsets for padding that flips automatically:
// This flips in RTL without extra code
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
)
Testing localizations
Widget buildLocalized(Widget child, {Locale locale = const Locale('en')}) {
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: child,
);
}
testWidgets('shows French translation', (tester) async {
await tester.pumpWidget(
buildLocalized(const HomeScreen(), locale: const Locale('fr')),
);
expect(find.text('Mon Application'), findsOneWidget);
});
Common pitfalls
Missing translations. If app_fr.arb is missing a key that app_en.arb has, the generator either errors or falls back to the template language. Set untranslated-messages-file in l10n.yaml to catch gaps.
Pluralization without ICU format. Simple string replacement doesn't handle plurals correctly ("1 items" instead of "1 item"). Always use ICU plural syntax in ARB files.
Hardcoded locale in tests. Tests that run in a fixed locale may pass in English but fail when the locale changes because a date or number is formatted differently. Use buildLocalized helpers with an explicit locale.
Sign in to like, dislike, or report.