Flutter Localization (i18n): Building Multi-Language Apps
# 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: ^class="code-number">0.19.class="code-number">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`:
{
class="code-string">"@@locale": class="code-string">"en",
class="code-string">"appTitle": class="code-string">"My Application",
class="code-string">"@appTitle": { class="code-string">"description": class="code-string">"The title of the application" },
class="code-string">"welcomeMessage": class="code-string">"Welcome, {userName}!",
class="code-string">"@welcomeMessage": {
class="code-string">"placeholders": { class="code-string">"userName": { class="code-string">"type": class="code-string">"String", class="code-string">"example": class="code-string">"John" } }
},
class="code-string">"itemCount": class="code-string">"{count, plural, =class="code-number">0{No items} =class="code-number">1{class="code-number">1 item} other{{count} items}}",
class="code-string">"@itemCount": {
class="code-string">"placeholders": { class="code-string">"count": { class="code-string">"type": class="code-string">"num", class="code-string">"format": class="code-string">"compact" } }
},
class="code-string">"lastLogin": class="code-string">"Last login: {date}",
class="code-string">"@lastLogin": {
class="code-string">"placeholders": { class="code-string">"date": { class="code-string">"type": class="code-string">"DateTime", class="code-string">"format": class="code-string">"yMMMd" } }
}
}Then create `lib/l10n/app_tr.arb` for Turkish:
{
class="code-string">"@@locale": class="code-string">"tr",
class="code-string">"appTitle": class="code-string">"Uygulamam",
class="code-string">"welcomeMessage": class="code-string">"Hos geldin, {userName}!",
class="code-string">"itemCount": class="code-string">"{count, plural, =class="code-number">0{Oge yok} =class="code-number">1{class="code-number">1 oge} other{{count} oge}}",
class="code-string">"lastLogin": class="code-string">"Son giris: {date}"
}Step 4: Configure MaterialApp
import class="code-string">'package:flutter_localizations/flutter_localizations.dart';
import class="code-string">'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(class="code-string">'en'), Locale(class="code-string">'tr'), Locale(class="code-string">'de')],
locale: const Locale(class="code-string">'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(class="code-string">'John')),
Text(l10n.itemCount(class="code-number">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(class="code-string">'en');
Locale get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}
class=class="code-string">"code-comment">// 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:
{
class="code-string">"notificationCount": class="code-string">"{count, plural, =class="code-number">0{No notifications} =class="code-number">1{class="code-number">1 notification} other{{count} notifications}}",
class="code-string">"@notificationCount": { class="code-string">"placeholders": { class="code-string">"count": { class="code-string">"type": class="code-string">"num" } } },
class="code-string">"genderGreeting": class="code-string">"{gender, select, male{Mr. {name}} female{Ms. {name}} other{Dear {name}}}",
class="code-string">"@genderGreeting": {
class="code-string">"placeholders": { class="code-string">"gender": { class="code-string">"type": class="code-string">"String" }, class="code-string">"name": { class="code-string">"type": class="code-string">"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 class="code-string">'package:intl/intl.dart';
final dateFormatter = DateFormat.yMMMd(class="code-string">'tr');
print(dateFormatter.format(DateTime.now())); class=class="code-string">"code-comment">// "class="code-number">9 Mar class="code-number">2026"
final currencyFormatter = NumberFormat.currency(locale: class="code-string">'de', symbol: class="code-string">'\u20ac');
print(currencyFormatter.format(class="code-number">1234.56)); class=class="code-string">"code-comment">// "class="code-number">1.234,class="code-number">56 \u20ac"
final compactFormatter = NumberFormat.compact(locale: class="code-string">'en');
print(compactFormatter.format(class="code-number">1500000)); class=class="code-string">"code-comment">// "class="code-number">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`:
class=class="code-string">"code-comment">// Bad - breaks in RTL
Padding(padding: EdgeInsets.only(left: class="code-number">16.0))
class=class="code-string">"code-comment">// Good - respects text direction
Padding(padding: EdgeInsetsDirectional.only(start: class="code-number">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:
{
class="code-string">"auth_loginButton": class="code-string">"Log In",
class="code-string">"auth_signupButton": class="code-string">"Sign Up",
class="code-string">"profile_editTitle": class="code-string">"Edit Profile",
class="code-string">"settings_languageLabel": class="code-string">"Language",
class="code-string">"error_networkTimeout": class="code-string">"Connection timed out. Please try again."
}Integrating with Translation Services
For production apps, I use one of these workflows:
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.
class=class="code-string">"code-comment">// Wrong
Text(class="code-string">'Welcome back!')
class=class="code-string">"code-comment">// Right
Text(AppLocalizations.of(context).welcomeBack)Mistake 2: String Concatenation
Never build sentences by concatenating translated fragments. Word order varies between languages.
class=class="code-string">"code-comment">// Wrong - word order breaks in other languages
Text(l10n.you + class="code-string">' ' + l10n.have + class="code-string">' ' + count.toString() + class="code-string">' ' + l10n.messages)
class=class="code-string">"code-comment">// 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
class=class="code-string">"code-comment">// Wrong - US-only format
Text(class="code-string">'${date.month}/${date.day}/${date.year}')
class=class="code-string">"code-comment">// 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(class="code-string">'displays Turkish welcome message', (tester) async {
await tester.pumpWidget(MaterialApp(
locale: const Locale(class="code-string">'tr'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
));
expect(find.text(class="code-string">'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