Flutter's cross-platform architecture makes it an excellent choice for building apps that reach global audiences. With first-class internationalization (i18n) support through the flutter_localizations package and ARB (Application Resource Bundle) files, Flutter provides a robust foundation for multilingual apps. This comprehensive guide covers everything from basic setup to advanced patterns like plural handling, gender support, platform-specific strings, and integration with modern translation management systems like IntlPull for streamlined workflows and over-the-air (OTA) updates.
Understanding Flutter's Localization Architecture
Flutter's i18n system is built around three core components:
- ARB Files: JSON-based resource bundles that store translations with metadata
- gen-l10n Tool: Code generator that creates type-safe Dart classes from ARB files
- flutter_localizations Package: Framework integration for locale detection and fallback
This architecture ensures type safety, prevents missing translation errors at compile time, and provides a developer-friendly API. Unlike manual string interpolation, Flutter's approach generates strongly-typed methods with IDE autocomplete support.
Setting Up flutter_localizations
Step 1: Add Dependencies
Update your pubspec.yaml:
YAML1dependencies: 2 flutter: 3 sdk: flutter 4 flutter_localizations: 5 sdk: flutter 6 intl: ^0.19.0 7 8flutter: 9 generate: true
Step 2: Configure l10n.yaml
Create l10n.yaml in your project root:
YAML1arb-dir: lib/l10n 2template-arb-file: app_en.arb 3output-localization-file: app_localizations.dart 4output-class: AppLocalizations 5nullable-getter: false
Configuration Options:
arb-dir: Directory containing ARB filestemplate-arb-file: Source language file (usually English)output-localization-file: Generated Dart file namenullable-getter: Set tofalseto avoid null checks
Step 3: Create ARB Files
Create lib/l10n/app_en.arb:
JSON1{ 2 "@@locale": "en", 3 "appTitle": "My App", 4 "@appTitle": { 5 "description": "Application title shown in app bar" 6 }, 7 "welcomeMessage": "Welcome, {userName}!", 8 "@welcomeMessage": { 9 "description": "Greeting shown on home screen", 10 "placeholders": { 11 "userName": { 12 "type": "String", 13 "example": "Alice" 14 } 15 } 16 }, 17 "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", 18 "@itemCount": { 19 "description": "Item count with plural support", 20 "placeholders": { 21 "count": { 22 "type": "int", 23 "format": "compact" 24 } 25 } 26 } 27}
Create lib/l10n/app_es.arb:
JSON1{ 2 "@@locale": "es", 3 "appTitle": "Mi Aplicación", 4 "welcomeMessage": "¡Bienvenido, {userName}!", 5 "itemCount": "{count, plural, =0{Sin artículos} =1{1 artículo} other{{count} artículos}}" 6}
Step 4: Generate Localization Code
Run code generation:
Terminalflutter gen-l10n
This creates .dart_tool/flutter_gen/gen_l10n/ with type-safe classes.
Step 5: Configure MaterialApp
Update lib/main.dart:
DART1import 'package:flutter/material.dart'; 2import 'package:flutter_localizations/flutter_localizations.dart'; 3import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 5void main() { 6 runApp(const MyApp()); 7} 8 9class MyApp extends StatelessWidget { 10 const MyApp({Key? key}) : super(key: key); 11 12 13 Widget build(BuildContext context) { 14 return MaterialApp( 15 title: 'Flutter i18n Demo', 16 localizationsDelegates: const [ 17 AppLocalizations.delegate, 18 GlobalMaterialLocalizations.delegate, 19 GlobalWidgetsLocalizations.delegate, 20 GlobalCupertinoLocalizations.delegate, 21 ], 22 supportedLocales: const [ 23 Locale('en'), // English 24 Locale('es'), // Spanish 25 Locale('fr'), // French 26 ], 27 home: const HomeScreen(), 28 ); 29 } 30}
Step 6: Use Translations in Widgets
DART1class HomeScreen extends StatelessWidget { 2 const HomeScreen({Key? key}) : super(key: key); 3 4 5 Widget build(BuildContext context) { 6 final l10n = AppLocalizations.of(context)!; 7 8 return Scaffold( 9 appBar: AppBar( 10 title: Text(l10n.appTitle), 11 ), 12 body: Center( 13 child: Column( 14 mainAxisAlignment: MainAxisAlignment.center, 15 children: [ 16 Text(l10n.welcomeMessage('Alice')), 17 const SizedBox(height: 20), 18 Text(l10n.itemCount(0)), 19 Text(l10n.itemCount(1)), 20 Text(l10n.itemCount(5)), 21 ], 22 ), 23 ), 24 ); 25 } 26}
Advanced ARB File Features
Plurals and Selects
Flutter supports ICU MessageFormat for complex grammar:
JSON1{ 2 "newMessages": "{count, plural, =0{No new messages} =1{1 new message} other{{count} new messages}}", 3 "gender": "{userGender, select, male{He replied} female{She replied} other{They replied}}" 4}
Usage in Dart:
DARTText(l10n.newMessages(5)); // "5 new messages" Text(l10n.gender('female')); // "She replied"
Date and Number Formatting
JSON1{ 2 "lastUpdated": "Last updated: {date}", 3 "@lastUpdated": { 4 "placeholders": { 5 "date": { 6 "type": "DateTime", 7 "format": "yMMMMd" 8 } 9 } 10 }, 11 "price": "Price: {amount}", 12 "@price": { 13 "placeholders": { 14 "amount": { 15 "type": "double", 16 "format": "currency", 17 "optionalParameters": { 18 "symbol": "$" 19 } 20 } 21 } 22 } 23}
Usage:
DARTText(l10n.lastUpdated(DateTime.now())); // "Last updated: February 12, 2026" Text(l10n.price(19.99)); // "Price: $19.99"
Nested Placeholders
JSON{ "greeting": "Hello {name}, you have {count, plural, =0{no messages} =1{1 message} other{{count} messages}}" }
Platform-Specific Translations
iOS vs Android String Differences
Create platform-aware translations:
JSON1{ 2 "tapAction": "{platform, select, ios{Tap} android{Tap} other{Click}}", 3 "shareAction": "{platform, select, ios{Share...} android{Share} other{Share}}" 4}
Detect Platform in Dart:
DART1import 'dart:io'; 2 3String getPlatform() { 4 if (Platform.isIOS) return 'ios'; 5 if (Platform.isAndroid) return 'android'; 6 return 'other'; 7} 8 9// Usage 10Text(l10n.tapAction(getPlatform()));
Platform Channels for Native Strings
For strings managed by native code:
DART1import 'package:flutter/services.dart'; 2 3class NativeLocalizations { 4 static const MethodChannel _channel = MethodChannel('app.localization'); 5 6 static Future<String> getString(String key) async { 7 try { 8 final String result = await _channel.invokeMethod('getString', {'key': key}); 9 return result; 10 } catch (e) { 11 return key; // Fallback to key 12 } 13 } 14}
iOS Implementation (Swift):
Swift1FlutterMethodChannel(name: "app.localization", binaryMessenger: controller.binaryMessenger) 2 .setMethodCallHandler { call, result in 3 if call.method == "getString", let key = call.arguments as? String { 4 result(NSLocalizedString(key, comment: "")) 5 } else { 6 result(FlutterMethodNotImplemented) 7 } 8 }
IntlPull CLI Integration
IntlPull streamlines Flutter localization with automated workflows and OTA updates.
Installation and Setup
Terminal1# Install CLI globally 2npm install -g @intlpullhq/cli 3 4# Initialize in Flutter project 5cd my_flutter_app 6intlpull init --framework flutter
Configuration (intlpull.config.json):
JSON1{ 2 "projectId": "your-project-id", 3 "apiKey": "ip_live_...", 4 "framework": "flutter", 5 "sourcePath": "lib/l10n", 6 "format": "arb", 7 "languages": ["en", "es", "fr", "de"], 8 "defaultLanguage": "en" 9}
Automated ARB Extraction
Terminal1# Scan Dart code for hardcoded strings 2intlpull scan --auto-wrap 3 4# Before: 5Text('Welcome to our app') 6 7# After: 8Text(AppLocalizations.of(context)!.welcome)
Push and Pull Translations
Terminal1# Upload ARB files to IntlPull 2intlpull push 3 4# Download latest translations 5intlpull pull 6 7# Watch for changes (real-time sync) 8intlpull watch
OTA Updates for Flutter
IntlPull's OTA SDK enables instant translation updates without app resubmission:
Terminal# Add dependency flutter pub add intlpull_ota
Integration:
DART1import 'package:intlpull_ota/intlpull_ota.dart'; 2 3void main() async { 4 WidgetsFlutterBinding.ensureInitialized(); 5 6 final ota = IntlPullOTA( 7 projectId: 'your-project-id', 8 apiKey: 'ip_live_...', 9 environment: 'production', 10 ); 11 12 await ota.initialize(); 13 14 runApp(MyApp(ota: ota)); 15} 16 17class MyApp extends StatelessWidget { 18 final IntlPullOTA ota; 19 const MyApp({required this.ota, Key? key}) : super(key: key); 20 21 22 Widget build(BuildContext context) { 23 return OTAProvider( 24 ota: ota, 25 child: MaterialApp( 26 localizationsDelegates: [ 27 OTALocalizationsDelegate(ota), 28 AppLocalizations.delegate, 29 // ... 30 ], 31 // ... 32 ), 33 ); 34 } 35}
OTA Workflow:
Terminal# Publish new release from CLI intlpull publish --version 1.2.0 --message "Fixed typos"
App fetches updates on launch (cached for 24 hours). Critical fixes deploy in minutes instead of days.
Testing Localization
Unit Tests for Translations
DART1import 'package:flutter_test/flutter_test.dart'; 2import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 4void main() { 5 test('Spanish translations exist', () async { 6 final localizations = await AppLocalizations.delegate.load(const Locale('es')); 7 expect(localizations.appTitle, 'Mi Aplicación'); 8 expect(localizations.welcomeMessage('Carlos'), contains('Carlos')); 9 }); 10 11 test('Plural forms work correctly', () async { 12 final localizations = await AppLocalizations.delegate.load(const Locale('en')); 13 expect(localizations.itemCount(0), 'No items'); 14 expect(localizations.itemCount(1), '1 item'); 15 expect(localizations.itemCount(5), '5 items'); 16 }); 17}
Widget Tests
DART1testWidgets('Home screen shows translated title', (tester) async { 2 await tester.pumpWidget( 3 MaterialApp( 4 localizationsDelegates: [ 5 AppLocalizations.delegate, 6 GlobalMaterialLocalizations.delegate, 7 ], 8 supportedLocales: const [Locale('en')], 9 home: const HomeScreen(), 10 ), 11 ); 12 13 expect(find.text('My App'), findsOneWidget); 14});
Manual Testing with Device Locale
Change device language in simulator/emulator:
iOS Simulator:
Settings → General → Language & Region → Add Language
Android Emulator:
Settings → System → Languages & input → Languages → Add a language
Golden Tests for Layout
Test UI doesn't break with longer translations:
DART1testWidgets('Button fits Spanish translation', (tester) async { 2 await tester.pumpWidget(/* Spanish locale widget */); 3 await expectLater( 4 find.byType(MaterialApp), 5 matchesGoldenFile('golden/button_es.png'), 6 ); 7});
Best Practices and Performance
1. Avoid Runtime Translation Lookups
Bad:
DART// Don't dynamically construct keys Text(l10n['button_' + action]); // Won't compile
Good:
DART1// Use explicit methods 2String getActionText(String action) { 3 switch (action) { 4 case 'save': return l10n.saveButton; 5 case 'cancel': return l10n.cancelButton; 6 default: return l10n.defaultButton; 7 } 8}
2. Lazy Load ARB Files
For large apps, split ARB files by feature:
YAML1# l10n.yaml 2arb-dir: lib/l10n 3template-arb-file: app_en.arb 4output-localization-file: app_localizations.dart 5synthetic-package: false
Create separate ARB files:
app_en.arb(global strings)home_en.arb(home screen)settings_en.arb(settings)
3. Cache Localizations in State
DART1class HomeScreen extends StatefulWidget { 2 3 State<HomeScreen> createState() => _HomeScreenState(); 4} 5 6class _HomeScreenState extends State<HomeScreen> { 7 late final AppLocalizations l10n; 8 9 10 void didChangeDependencies() { 11 super.didChangeDependencies(); 12 l10n = AppLocalizations.of(context)!; 13 } 14 15 16 Widget build(BuildContext context) { 17 return Text(l10n.appTitle); // Cached lookup 18 } 19}
4. Use intl_utils for Tooling
Install intl_utils for better DX:
Terminalflutter pub global activate intl_utils
Add to pubspec.yaml:
YAMLflutter_intl: enabled: true
5. Validate ARB Files in CI/CD
Terminal1# Check for missing translations 2intlpull validate --missing 3 4# Check for unused keys 5intlpull validate --unused 6 7# Run in CI 8- name: Validate Translations 9 run: | 10 npm install -g @intlpullhq/cli 11 intlpull validate --missing --unused --fail-on-error
Common Pitfalls and Solutions
Issue 1: "Locale Not Found" Error
Cause: Locale declared in supportedLocales but ARB file missing.
Solution:
DART1supportedLocales: const [ 2 Locale('en'), 3 Locale('es'), 4 // Don't add locales without ARB files 5],
Issue 2: Plural Forms Not Working
Cause: Incorrect ICU MessageFormat syntax.
Solution:
JSON1// Wrong 2"items": "{count} items" 3 4// Correct 5"items": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"
Issue 3: Hot Reload Doesn't Update Translations
Cause: Generated code not refreshed.
Solution:
Terminal1# Regenerate localizations 2flutter gen-l10n 3 4# Full restart required (not hot reload) 5flutter run
Issue 4: RTL Layout Issues
Solution: Test with RTL languages (Arabic, Hebrew):
DART1MaterialApp( 2 localizationsDelegates: [...], 3 supportedLocales: [ 4 const Locale('en'), 5 const Locale('ar'), // Right-to-left 6 ], 7 builder: (context, child) { 8 return Directionality( 9 textDirection: Localizations.localeOf(context).languageCode == 'ar' 10 ? TextDirection.rtl 11 : TextDirection.ltr, 12 child: child!, 13 ); 14 }, 15)
Production Deployment Checklist
- All ARB files contain same keys
- Plural forms tested for all languages
- Date/number formats verified per locale
- RTL layouts tested (if supporting Arabic/Hebrew)
- Platform-specific strings validated on iOS and Android
- IntlPull OTA SDK configured for production
- CI/CD validates translations before merge
- Fallback locale configured (
enrecommended) - Translation memory populated for reuse
- Team trained on ARB file editing
Frequently Asked Questions
How do I handle region-specific locales (e.g., en_US vs en_GB)?
Use full locale codes in ARB files:
lib/l10n/app_en_US.arb
lib/l10n/app_en_GB.arb
DART1supportedLocales: const [ 2 Locale('en', 'US'), 3 Locale('en', 'GB'), 4],
Can I use JSON instead of ARB files?
ARB is required for gen-l10n. However, IntlPull CLI can convert JSON to ARB:
Terminalintlpull convert --from json --to arb --input translations/ --output lib/l10n/
How do I translate app name and permissions?
App names require platform-specific configuration:
iOS (ios/Runner/Info.plist):
XML<key>CFBundleDisplayName</key> <string>$(APP_NAME)</string>
Android (android/app/src/main/res/values-es/strings.xml):
XML<string name="app_name">Mi Aplicación</string>
What's the performance impact of many translations?
Minimal. Flutter loads only active locale. For 10,000+ keys, consider namespace splitting.
How do I test translations without changing device locale?
Force locale programmatically:
DART1MaterialApp( 2 locale: const Locale('es'), // Force Spanish 3 localizationsDelegates: [...], 4)
Can I use IntlPull with existing ARB files?
Yes. Run intlpull init in your Flutter project, and existing ARB files will be detected and imported.
How often should I publish OTA updates?
- Critical fixes (typos in checkout): Immediately
- New features: With app releases
- Seasonal campaigns: As needed
- A/B tests: Daily/weekly
Recommended cadence: Weekly maintenance releases, immediate for critical bugs.
Conclusion
Flutter's i18n architecture provides a robust foundation for building multilingual apps that scale globally. By leveraging ARB files, the gen-l10n tool, and modern translation management platforms like IntlPull, you can reduce localization overhead by 80% while delivering instant updates to users. The type-safe, compile-time validation ensures translation bugs are caught before production, and OTA updates eliminate the painful app store submission cycle for translation fixes.
Start with the basics (ARB files + flutter_localizations), gradually adopt advanced features (plurals, platform overrides), and integrate IntlPull CLI for team collaboration and automated workflows. Your users across the globe will appreciate the attention to linguistic detail, and your development team will appreciate the time saved.
Ready to streamline your Flutter localization workflow? Try IntlPull free with 500 keys and 3 languages, or explore our Flutter-specific documentation for more advanced patterns.
