Internationalization & Localization
flutter_localizations, ARB files, intl package, RTL, plurals, dynamic locale switching
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Internationalization and localization make your app usable by people in different countries and languages. For global products — SaaS tools, NGO field apps, fintech platforms — this is a day-one architectural decision. Retrofitting i18n into an established codebase is expensive; doing it from the start is cheap.
Real-world relevance
The NGO survey app deploys in 12 countries. Each field worker sees their device's language automatically. Arabic workers see RTL layout. Survey question counts use proper plural rules ('1 question remaining' vs '3 questions remaining'). Sync timestamps are formatted in local date order. The translation team updates ARB files via Lokalise; CI runs flutter gen-l10n and fails if any keys are missing.
Key points
- i18n vs l10n — Internationalization (i18n) is engineering the app to support multiple locales — dates, currencies, text direction, string externalisation. Localization (l10n) is the process of providing actual translations and locale-specific values for each target locale.
- flutter_localizations Setup — Add flutter_localizations to pubspec.yaml. Set supportedLocales in MaterialApp. Add generate: true and l10n.yaml config. Run flutter gen-l10n to generate the AppLocalizations class from ARB files.
- ARB Files — Application Resource Bundle (.arb) files are JSON-like files containing string keys with translations: { 'greetUser': 'Hello, {name}!', '@greetUser': { 'description': 'Greeting', 'placeholders': { 'name': { 'type': 'String' } } } }. One ARB per locale (app_en.arb, app_ar.arb, app_fr.arb).
- intl Package — Provides DateFormat, NumberFormat, Intl.plural(), Intl.gender(), Intl.select() for locale-aware formatting. Always format dates and numbers through intl — never concatenate locale-sensitive values as strings.
- Date & Number Formatting — DateFormat('d MMMM y', locale).format(date) respects locale date order. NumberFormat.currency(locale: locale, symbol: '€').format(amount) formats for locale. Critical for fintech apps showing claim amounts and transaction dates.
- Plural Rules — Different languages have different plural forms. English: one/other. Arabic: zero/one/two/few/many/other. Use Intl.plural() or ARB plural syntax: { 'itemCount': '{count, plural, =0{No items} =1{1 item} other{{count} items}}' }. Never hardcode 'items' after a number.
- RTL Support — Arabic, Hebrew, Urdu require right-to-left layout. Set locale to 'ar' and Flutter automatically flips Row direction, padding direction, and text alignment. Use Directionality.of(context) to detect RTL. Test with Arabic locale on a real device.
- Dynamic Locale Switching — Allow users to change language in-app without restarting. Store chosen locale in SharedPreferences. Pass a locale override to MaterialApp via a ValueNotifier or a Riverpod/BLoC state. Rebuild MaterialApp when locale changes.
- Translation Workflow — Export ARB files to translators. Use Lokalise, Phrase, or POEditor for professional translation management. Re-import translated ARBs. Run flutter gen-l10n. Review plurals and gendered strings — machine translation often gets these wrong.
- Fallback Locale — MaterialApp.localeResolutionCallback handles cases where the user's device locale has no matching translation. Default: fall back to the first entry in supportedLocales. Implement custom resolution for region fallbacks (e.g., 'pt_BR' falls back to 'pt').
Code example
// === l10n.yaml ===
/*
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
*/
// === app_en.arb ===
/*
{
"appTitle": "Survey App",
"@appTitle": { "description": "Application title" },
"greetUser": "Hello, {name}!",
"@greetUser": {
"placeholders": { "name": { "type": "String" } }
},
"pendingCount": "{count, plural, =0{No pending surveys} =1{1 pending survey} other{{count} pending surveys}}",
"@pendingCount": {
"placeholders": { "count": { "type": "int" } }
},
"claimAmount": "Amount: {amount}",
"@claimAmount": {
"placeholders": { "amount": { "type": "String" } }
}
}
*/
// === app_ar.arb ===
/*
{
"appTitle": "تطبيق الاستبيان",
"greetUser": "مرحباً، {name}!",
"pendingCount": "{count, plural, =0{لا توجد استبيانات} =1{استبيان واحد} =2{استبيانان} few{{count} استبيانات} many{{count} استبياناً} other{{count} استبيان}}",
"claimAmount": "المبلغ: {amount}"
}
*/
// === MaterialApp setup ===
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Locale? _locale;
void _setLocale(Locale locale) {
setState(() => _locale = locale);
SharedPreferences.getInstance().then(
(prefs) => prefs.setString('locale', locale.languageCode),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: _locale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
localeResolutionCallback: (deviceLocale, supportedLocales) {
// Custom fallback: pt_BR -> pt -> en
if (deviceLocale == null) return supportedLocales.first;
for (final locale in supportedLocales) {
if (locale.languageCode == deviceLocale.languageCode) return locale;
}
return supportedLocales.first; // Default: English
},
home: HomeScreen(onLocaleChange: _setLocale),
);
}
}
// === USING LOCALIZATIONS ===
class HomeScreen extends StatelessWidget {
final void Function(Locale) onLocaleChange;
const HomeScreen({required this.onLocaleChange, super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(children: [
Text(l10n.greetUser(name: 'Ahmad')),
Text(l10n.pendingCount(count: 3)),
// Locale-aware number formatting
Text(NumberFormat.currency(
locale: locale.toLanguageTag(), symbol: '$',
).format(1234567.89)), // -> $1,234,567.89 or locale equivalent
// Locale-aware date formatting
Text(DateFormat.yMMMMd(locale.toLanguageTag())
.format(DateTime.now())),
// Language switcher
DropdownButton<Locale>(
value: locale,
items: AppLocalizations.supportedLocales.map((l) =>
DropdownMenuItem(value: l, child: Text(l.languageCode))
).toList(),
onChanged: (l) { if (l != null) onLocaleChange(l); },
),
]),
);
}
}Line-by-line walkthrough
- 1. l10n.yaml tells flutter gen-l10n where ARB files live and what output file to generate — must be at project root
- 2. @greetUser object in ARB provides metadata (description, placeholder types) that gen-l10n uses to generate type-safe method signatures
- 3. Plural ARB syntax {count, plural, =0{...} =1{...} other{...}} — ICU message format that intl applies correct CLDR plural rules per locale
- 4. AppLocalizations.of(context)! — retrieves the localizations instance; throws if localizationsDelegates not configured in MaterialApp
- 5. Localizations.localeOf(context) — reads the current active locale for use in intl formatting calls
- 6. NumberFormat.currency(locale: locale.toLanguageTag()) — formats according to local numeric conventions (decimal/thousands separators)
- 7. DateFormat.yMMMMd(locale.toLanguageTag()) — formats date in locale order (e.g., January 15, 2025 in en; 15 janvier 2025 in fr)
- 8. localeResolutionCallback implements custom fallback logic — critical for regional variants like pt_BR not having a separate ARB
Spot the bug
// In HomeScreen
Widget build(BuildContext context) {
final count = pendingSurveys.length;
return Text(
count == 1
? '1 pending survey'
: '$count pending surveys',
);
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Flutter internationalization guide (Flutter Docs)
- intl package (pub.dev)
- ARB file format (GitHub)
- CLDR plural rules (Unicode CLDR)
- flutter_localizations API (Flutter Docs)