Flutter
Flutter Localization (i18n): Building Multi-Language Apps
Localization is not a nice-to-have — it is a core product capability for any app targeting a global audience. In apps I've shipped to 10+ markets, getting i18n right from day one saved weeks of refactoring later. This guide walks through a complete Flutter localization setup, from initial configuration to RTL support and team translation workflows.
Setting Up Localization from Scratch
Step 1: Add Dependencies
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
generate: trueThe generate: true flag tells Flutter to auto-generate localization code from your ARB files.
Step 2: Create the l10n Configuration
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: falseStep 3: Create ARB Files
ARB (Application Resource Bundle) files are JSON-based and serve as your single source of truth. Create lib/l10n/app_en.arb:
{
"@@locale": "en",
"appTitle": "My Application",
"@appTitle": { "description": "The title of the application" },
"welcomeMessage": "Welcome, {userName}!",
"@welcomeMessage": {
"placeholders": { "userName": { "type": "String", "example": "John" } }
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"placeholders": { "count": { "type": "num", "format": "compact" } }
},
"lastLogin": "Last login: {date}",
"@lastLogin": {
"placeholders": { "date": { "type": "DateTime", "format": "yMMMd" } }
}
}Then create lib/l10n/app_tr.arb for Turkish:
{
"@@locale": "tr",
"appTitle": "Uygulamam",
"welcomeMessage": "Hos geldin, {userName}!",
"itemCount": "{count, plural, =0{Oge yok} =1{1 oge} other{{count} oge}}",
"lastLogin": "Son giris: {date}"
}Step 4: Configure MaterialApp
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('en'), Locale('tr'), Locale('de')],
locale: const Locale('en'),
home: const HomeScreen(),
);
}
}Step 5: Access Translations in Widgets
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: Column(children: [
Text(l10n.welcomeMessage('John')),
Text(l10n.itemCount(5)),
Text(l10n.lastLogin(DateTime.now())),
]),
);
}
}Dynamic Locale Switching
Users should be able to switch languages without restarting the app. A clean pattern uses a ChangeNotifier:
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('en');
Locale get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}
// In your MaterialApp:
Consumer<LocaleProvider>(
builder: (context, provider, _) => MaterialApp(
locale: provider.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
),
)Pluralization and Gender
The ICU message format handles pluralization elegantly. Flutter supports plural, select, and gender forms directly in ARB files:
{
"notificationCount": "{count, plural, =0{No notifications} =1{1 notification} other{{count} notifications}}",
"@notificationCount": { "placeholders": { "count": { "type": "num" } } },
"genderGreeting": "{gender, select, male{Mr. {name}} female{Ms. {name}} other{Dear {name}}}",
"@genderGreeting": {
"placeholders": { "gender": { "type": "String" }, "name": { "type": "String" } }
}
}Languages like Polish, Arabic, and Russian have complex plural rules beyond singular/plural. The ICU format handles zero, one, two, few, many, and other categories automatically based on locale.
Date and Number Formatting
Never format dates or numbers manually. Always use intl:
import 'package:intl/intl.dart';
final dateFormatter = DateFormat.yMMMd('tr');
print(dateFormatter.format(DateTime.now())); // "9 Mar 2026"
final currencyFormatter = NumberFormat.currency(locale: 'de', symbol: '\u20ac');
print(currencyFormatter.format(1234.56)); // "1.234,56 \u20ac"
final compactFormatter = NumberFormat.compact(locale: 'en');
print(compactFormatter.format(1500000)); // "1.5M"RTL Language Support
Supporting right-to-left languages like Arabic, Hebrew, and Farsi requires careful layout attention.
Flutter handles text direction automatically via flutter_localizations. The key rule: replace hardcoded left/right with start/end:
// Bad - breaks in RTL
Padding(padding: EdgeInsets.only(left: 16.0))
// Good - respects text direction
Padding(padding: EdgeInsetsDirectional.only(start: 16.0))Some icons need mirroring in RTL layouts:
Widget buildArrowIcon(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Transform.flip(flipX: isRtl, child: const Icon(Icons.arrow_forward));
}In apps I've shipped to 10+ markets, I always test with Arabic even if the app does not officially support it, just to catch directional layout bugs early.
Translation Workflow for Teams
Organizing ARB Files
I recommend domain-based key naming to keep things manageable as projects grow:
{
"auth_loginButton": "Log In",
"auth_signupButton": "Sign Up",
"profile_editTitle": "Edit Profile",
"settings_languageLabel": "Language",
"error_networkTimeout": "Connection timed out. Please try again."
}Integrating with Translation Services
For production apps, I use one of these workflows:
- Manual translation: Export ARBs, send to translators, import back. Works for small projects with fewer than 100 strings.
- Localizely / Crowdin / Phrase: Upload your template ARB, translators work in a web UI, download completed ARBs. These tools handle plural rules and flag missing translations.
- CI/CD integration: A GitHub Action that checks for missing keys on every PR, preventing incomplete translations from shipping.
Missing Translation Detection
A simple Dart script can parse all ARB files, compare keys against the template, and fail CI if any translations are missing. I keep a scripts/check_translations.dart in every project that does exactly this — it reads app_en.arb as the source of truth and diffs every other ARB file against it.
Common i18n Mistakes
Mistake 1: Hardcoded Strings
Every user-facing string must go through the localization system — including error messages, tooltips, and accessibility labels.
// Wrong
Text('Welcome back!')
// Right
Text(AppLocalizations.of(context).welcomeBack)Mistake 2: String Concatenation
Never build sentences by concatenating translated fragments. Word order varies between languages.
// Wrong - word order breaks in other languages
Text(l10n.you + ' ' + l10n.have + ' ' + count.toString() + ' ' + l10n.messages)
// Right - use a single parameterized string
Text(l10n.messageCount(count))Mistake 3: No Fallback Locale
If a translation is missing and there is no fallback, your app may crash or show raw keys. Flutter automatically falls back to the first locale in supportedLocales, so always put your primary language first.
Mistake 4: Ignoring Text Expansion
German text is typically 30% longer than English. Arabic can be even wider. Always test with long translations. I use pseudo-localization during development — wrapping strings with extra characters to simulate expansion.
Mistake 5: Hardcoded Date and Number Formats
// Wrong - US-only format
Text('${date.month}/${date.day}/${date.year}')
// Right - locale-aware
Text(DateFormat.yMd(Localizations.localeOf(context).toString()).format(date))Mistake 6: Forgetting Accessibility
Screen readers rely on localized Semantics labels. If you only localize visible text, visually impaired users in other locales get a broken experience:
Semantics(
label: AppLocalizations.of(context).closeButtonLabel,
child: IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
)Testing Localization
testWidgets('displays Turkish welcome message', (tester) async {
await tester.pumpWidget(MaterialApp(
locale: const Locale('tr'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
));
expect(find.text('Hos geldin, John!'), findsOneWidget);
});Conclusion
A solid i18n setup reduces release friction significantly when entering new markets. The investment you make early in proper ARB structure, locale-aware formatting, and a clean translation workflow pays for itself many times over. In apps I've shipped to 10+ markets, the teams that treated localization as a first-class architectural concern scaled smoothly to new regions without emergency hotfixes.
I can help you implement a scalable localization pipeline — from initial ARB setup to CI-integrated translation workflows.
Related Articles
What is Flutter? A Complete Beginner's Guide
Learn what Flutter is, how it works, and why modern product teams choose it. Discover Dart, widget architecture, and practical multi-platform development.
Flutter Navigation: Scalable Routing with go_router
Build modern Flutter navigation with go_router, including nested routes, redirects, and deep links.
Flutter Accessibility (a11y): Building Apps for Everyone
Apply accessibility standards in Flutter using semantics, contrast, text scaling, and keyboard-friendly interactions.
Have a Flutter Project?
I build high-performance Flutter applications for iOS, Android, and web.
Get in Touch