Flutter Localization Complete Guide 2026: From Setup to Production
Flutter localization lets you build apps that adapt to different languages, regions, and cultures. Whether you are adding your first translation or scaling to dozens of locales, this guide covers every step from initial setup to production deployment with practical examples and proven patterns.
Why Localize Your Flutter App
Expanding to new markets is one of the fastest ways to grow your app's user base. Users are far more likely to install and engage with an app in their native language. The Google Play Store and Apple App Store both rank localized apps higher in local search results. Flutter's built-in localization system makes this straightforward.
Benefits of localizing your Flutter app:
- Larger audience: Reach users who prefer or require non-English interfaces
- Higher retention: Users stay longer in apps that speak their language
- Better store ranking: App stores favor localized metadata and content
- Legal compliance: Some markets require apps to support the local language
- Competitive edge: Many apps skip localization, leaving an opportunity
Setting Up Flutter Localization
Step 1: Add Dependencies
Add the localization packages to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: true
The flutter_localizations package provides built-in translations for Material and Cupertino widgets. The intl package handles date, number, and message formatting.
Step 2: Configure l10n.yaml
Create an l10n.yaml file in your project root:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: true
nullable-getter: false
This tells Flutter where to find your translation files and how to generate the localization classes.
Step 3: Create ARB Files
ARB (Application Resource Bundle) is the standard format for Flutter translations. Create your template file at lib/l10n/app_en.arb:
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The title of the application"
},
"welcomeMessage": "Welcome, {userName}!",
"@welcomeMessage": {
"description": "Welcome message shown on the home screen",
"placeholders": {
"userName": {
"type": "String",
"example": "John"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Shows the number of items",
"placeholders": {
"count": {
"type": "int"
}
}
}
}
Then create translation files for each supported locale. For example, lib/l10n/app_ar.arb:
{
"@@locale": "ar",
"appTitle": "تطبيقي",
"welcomeMessage": "!{userName} مرحبًا",
"itemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{{count} عناصر} many{{count} عنصرًا} other{{count} عنصر}}"
}
Step 4: Configure MaterialApp
Wire the localizations into your MaterialApp:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
);
}
}
Step 5: Use Translations in Widgets
Access translations through AppLocalizations.of(context):
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(l10n.welcomeMessage('Flutter Developer')),
Text(l10n.itemCount(42)),
],
),
),
);
}
}
Run flutter gen-l10n or let the build system generate the localization files automatically.
Working with ARB Files
Placeholders
Placeholders let you insert dynamic values into translated strings:
{
"greeting": "Hello, {name}! You have {count} new messages.",
"@greeting": {
"placeholders": {
"name": { "type": "String" },
"count": { "type": "int" }
}
}
}
Pluralization
Flutter supports ICU plural syntax for handling different quantity forms:
{
"emailCount": "{count, plural, =0{No emails} =1{1 email} other{{count} emails}}",
"@emailCount": {
"placeholders": {
"count": { "type": "int" }
}
}
}
Arabic, for example, has six plural forms (zero, one, two, few, many, other), so provide all of them:
{
"emailCount": "{count, plural, =0{لا رسائل} =1{رسالة واحدة} =2{رسالتان} few{{count} رسائل} many{{count} رسالة} other{{count} رسالة}}"
}
Select Messages
Use select for gender-specific or category-based text:
{
"userRole": "{role, select, admin{Administrator} editor{Editor} viewer{Viewer} other{User}}",
"@userRole": {
"placeholders": {
"role": { "type": "String" }
}
}
}
Number and Date Formatting
Use the intl package for locale-aware formatting:
import 'package:intl/intl.dart';
class FormattedDataExample extends StatelessWidget {
const FormattedDataExample({super.key});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
final price = NumberFormat.currency(
locale: locale,
symbol: '\$',
).format(1234.56);
final date = DateFormat.yMMMMd(locale).format(DateTime.now());
return Column(
children: [
Text(price), // $1,234.56 in en, 1.234,56 $ in de
Text(date), // March 3, 2026 in en, 3 مارس 2026 in ar
],
);
}
}
Right-to-Left (RTL) Support
RTL languages like Arabic, Hebrew, and Persian require layout mirroring. Flutter handles most RTL adaptation automatically, but you should follow these patterns.
Use Directional Widgets
Replace fixed-direction properties with directional equivalents:
// Instead of EdgeInsets, use EdgeInsetsDirectional
Padding(
padding: EdgeInsetsDirectional.only(start: 16, end: 8),
child: Text(l10n.menuItem),
)
// Instead of Alignment, use AlignmentDirectional
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.label),
)
// Instead of BorderRadius, use BorderRadiusDirectional
Container(
decoration: BoxDecoration(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(12),
),
),
)
Check Text Direction
When you need conditional logic based on layout direction:
final isRtl = Directionality.of(context) == TextDirection.rtl;
Icon(isRtl ? Icons.arrow_back : Icons.arrow_forward)
Test RTL Layouts
Force RTL in tests or debug mode:
Directionality(
textDirection: TextDirection.rtl,
child: MyWidget(),
)
Changing Language at Runtime
Let users switch languages without restarting the app:
class LocaleProvider extends ChangeNotifier {
Locale? _locale;
Locale? get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
void clearLocale() {
_locale = null;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LocaleProvider(),
child: Consumer<LocaleProvider>(
builder: (context, provider, _) {
return MaterialApp(
locale: provider.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
);
},
),
);
}
}
Persist Language Selection
Save the user's language choice using SharedPreferences:
import 'package:shared_preferences/shared_preferences.dart';
class LocaleService {
static const _key = 'selected_locale';
static Future<void> saveLocale(String languageCode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, languageCode);
}
static Future<Locale?> getSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final code = prefs.getString(_key);
return code != null ? Locale(code) : null;
}
}
Localization Without Context
Sometimes you need translations outside the widget tree. Here are reliable patterns.
Using a Global Navigator Key
final navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
);
}
}
// Access translations anywhere
AppLocalizations get l10n =>
AppLocalizations.of(navigatorKey.currentContext!)!;
Using a Static Extension
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
// Usage in widgets
Text(context.l10n.welcomeMessage('User'))
Testing Localized Apps
Unit Testing Translations
Verify that all locale files have matching keys and no missing translations:
import 'dart:convert';
import 'dart:io';
import 'package:test/test.dart';
void main() {
test('All ARB files have matching keys', () {
final l10nDir = Directory('lib/l10n');
final arbFiles = l10nDir.listSync().whereType<File>().where(
(f) => f.path.endsWith('.arb'),
);
final templateFile = arbFiles.firstWhere(
(f) => f.path.contains('app_en.arb'),
);
final templateKeys = (jsonDecode(templateFile.readAsStringSync())
as Map<String, dynamic>)
.keys
.where((k) => !k.startsWith('@'))
.toSet();
for (final file in arbFiles) {
final keys = (jsonDecode(file.readAsStringSync())
as Map<String, dynamic>)
.keys
.where((k) => !k.startsWith('@'))
.toSet();
final missing = templateKeys.difference(keys);
expect(missing, isEmpty,
reason: '${file.path} is missing keys: $missing');
}
});
}
Widget Testing with Locales
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Widget buildLocalizedWidget(Widget child, {Locale locale = const Locale('en')}) {
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: child,
);
}
void main() {
testWidgets('Widget shows English text', (tester) async {
await tester.pumpWidget(buildLocalizedWidget(const HomeScreen()));
await tester.pumpAndSettle();
expect(find.text('My App'), findsOneWidget);
});
testWidgets('Widget shows Arabic text', (tester) async {
await tester.pumpWidget(
buildLocalizedWidget(const HomeScreen(), locale: const Locale('ar')),
);
await tester.pumpAndSettle();
expect(find.text('تطبيقي'), findsOneWidget);
});
}
Production Deployment Checklist
Before releasing your localized Flutter app:
Verify all translations are complete: Run
flutter gen-l10nand check for warnings about missing translations.Test every supported locale: Run through core user flows in each language to catch layout overflow, text truncation, and formatting issues.
Validate RTL layouts: Test with Arabic or Hebrew to ensure mirroring works, icons swap correctly, and no hardcoded left/right values remain.
Check pluralization rules: Verify plural forms for languages with complex rules (Arabic has six forms, Polish has four).
Test number and date formatting: Ensure currencies, dates, and numbers display correctly for each locale.
Localize app store metadata: Translate your app title, description, screenshots, and keywords for each target market.
Set up fallback locale: Configure a fallback locale for unsupported language codes to avoid crashes.
Measure text overflow: Long translations (German averages 30% longer than English) can break fixed-width layouts.
Common Pitfalls and Solutions
Missing Translations Crash
Problem: AppLocalizations.of(context) returns null.
Solution: Ensure localizationsDelegates and supportedLocales are set on MaterialApp, and the widget is below it in the tree.
Text Overflow in Other Languages
Problem: German or Finnish translations overflow containers.
Solution: Use Expanded, Flexible, or FittedBox instead of fixed widths. Test with the longest expected translation.
Hardcoded Strings
Problem: Some text stays in English after adding translations.
Solution: Search your codebase for string literals in widget code. Every user-visible string should come from AppLocalizations.
Wrong Plural Form
Problem: "1 items" appears instead of "1 item".
Solution: Use ICU plural syntax in ARB files and provide all required plural forms for each language.
RTL Icon Direction
Problem: Forward arrows point left in RTL mode.
Solution: Use Directionality.of(context) to conditionally flip directional icons.
Conclusion
Flutter localization transforms your app from single-language to globally accessible. Start with the setup outlined here, add translations incrementally, and test each locale before release. The combination of ARB files, generated localization classes, and Flutter's built-in RTL support gives you everything needed to build apps that feel native in every language.