This guide covers internationalization patterns for Flutter apps using this template.
We use Flutter's official localization approach:
- ARB files (Application Resource Bundle) for translation strings
- flutter_localizations for Material/Cupertino widget translations
- gen-l10n for code generation
This approach is recommended by the Flutter team and provides:
- Type-safe access to translations
- Compile-time checking for missing translations
- Support for pluralization, gender, and placeholders
- IDE autocomplete for translation keys
lib/
├── l10n/
│ ├── app_en.arb # English (source language)
│ ├── app_es.arb # Spanish
│ ├── app_fr.arb # French
│ └── generated/ # Auto-generated (gitignored)
│ └── app_localizations.dart
└── main.dart # Configured with delegates
The l10n.yaml file in the project root configures code generation:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
output-dir: lib/l10n/generated
nullable-getter: falseEnsure these are configured:
dependencies:
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: trueARB files are JSON with metadata. The English file (app_en.arb) is the source of truth.
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The title shown in the app bar"
}
}{
"welcomeMessage": "Welcome, {name}!",
"@welcomeMessage": {
"description": "Greeting shown after login",
"placeholders": {
"name": {
"type": "String",
"example": "John"
}
}
}
}Usage: AppLocalizations.of(context)!.welcomeMessage('John')
{
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Shows the number of items",
"placeholders": {
"count": {
"type": "int"
}
}
}
}Usage: AppLocalizations.of(context)!.itemCount(5) → "5 items"
{
"userGreeting": "{gender, select, male{He is online} female{She is online} other{They are online}}",
"@userGreeting": {
"placeholders": {
"gender": {
"type": "String"
}
}
}
}{
"priceLabel": "Price: {price}",
"@priceLabel": {
"placeholders": {
"price": {
"type": "double",
"format": "currency",
"optionalParameters": {
"symbol": "$",
"decimalDigits": 2
}
}
}
},
"lastUpdated": "Updated: {date}",
"@lastUpdated": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
}
}import 'package:flutter_template/l10n/generated/app_localizations.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Text(l10n.appTitle),
Text(l10n.welcomeMessage('John')),
Text(l10n.itemCount(5)),
],
);
}
}extension BuildContextL10n on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
// Usage
Text(context.l10n.appTitle)Use camelCase with descriptive names:
| Pattern | Example | Description |
|---|---|---|
{screen}{element} |
homeTitle, loginButton |
Screen-specific strings |
{action}Message |
errorMessage, successMessage |
Status messages |
button{Action} |
buttonSave, buttonCancel |
Button labels |
label{Field} |
labelEmail, labelPassword |
Form field labels |
hint{Field} |
hintEmail, hintPassword |
Input hints |
error{Type} |
errorRequired, errorInvalidEmail |
Validation errors |
Always include @key metadata with descriptions. This helps translators understand context:
{
"deleteConfirmation": "Are you sure you want to delete this?",
"@deleteConfirmation": {
"description": "Shown in confirmation dialog when user tries to delete an item"
}
}{
"newFeatureTitle": "New Feature",
"@newFeatureTitle": {
"description": "Title for the new feature screen"
}
}make l10n
# or
flutter gen-l10nText(AppLocalizations.of(context)!.newFeatureTitle)Update each app_*.arb file with the translation.
- Create
lib/l10n/app_{locale}.arb(e.g.,app_fr.arbfor French) - Copy structure from
app_en.arb - Translate all strings
- Run
flutter gen-l10n - The new locale is automatically available
Flutter handles RTL automatically when you add RTL locales (Arabic, Hebrew, etc.).
For custom layouts that need RTL awareness:
final isRtl = Directionality.of(context) == TextDirection.rtl;
// Or use directional widgets
Padding(
padding: EdgeInsetsDirectional.only(start: 16),
child: Text('Aligned to start'),
)Use the intl package for locale-aware formatting:
import 'package:intl/intl.dart';
// Get current locale
final locale = Localizations.localeOf(context).toString();
// Format date
final dateFormat = DateFormat.yMMMd(locale);
final formattedDate = dateFormat.format(DateTime.now());
// Format currency
final currencyFormat = NumberFormat.currency(locale: locale, symbol: '\$');
final formattedPrice = currencyFormat.format(19.99);
// Format number
final numberFormat = NumberFormat.decimalPattern(locale);
final formattedNumber = numberFormat.format(1234567);- Add strings to
app_en.arbas you build features - Run
make l10nto regenerate - Use type-safe accessors in code
- Before release, export ARB files to translators
Option 1: Direct ARB Editing
- Share ARB files directly
- Translators edit JSON (simple but error-prone)
Option 2: Translation Management Platforms
- Crowdin - Popular, good Flutter support
- Lokalise - Developer-focused
- POEditor - Simple and affordable
- Phrase - Enterprise features
These platforms:
- Import/export ARB files
- Provide translator-friendly UI
- Track translation progress
- Support translation memory
Check for missing translations in CI:
# scripts/l10n/check-missing.sh
./scripts/l10n/check-missing.shThis compares all ARB files against the English source and reports missing keys.
{
"errorNetwork": "Network error. Please check your connection.",
"errorServerCode": "Server error (code: {code})",
"@errorServerCode": {
"placeholders": {
"code": {"type": "int"}
}
}
}{
"timeAgo": "{time} ago",
"timeJustNow": "Just now",
"timeMinutes": "{count, plural, =1{1 minute} other{{count} minutes}}",
"timeHours": "{count, plural, =1{1 hour} other{{count} hours}}",
"timeDays": "{count, plural, =1{1 day} other{{count} days}}"
}{
"emptyListTitle": "No items yet",
"emptyListSubtitle": "Add your first item to get started",
"emptySearchTitle": "No results found",
"emptySearchSubtitle": "Try a different search term"
}testWidgets('displays localized title', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: MyWidget(),
),
);
expect(find.text('My App'), findsOneWidget);
});for (final locale in ['en', 'es', 'fr']) {
testWidgets('renders correctly in $locale', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale(locale),
home: MyWidget(),
),
);
// Verify no overflow or layout issues
expect(tester.takeException(), isNull);
});
}Ensure your widget is below MaterialApp in the widget tree and localization delegates are configured.
Run flutter gen-l10n or make l10n after modifying ARB files.
Check the ICU message syntax. Common mistakes:
- Missing
othercase (required) - Wrong brace matching
- Incorrect placeholder type
The file is at lib/l10n/generated/app_localizations.dart. Run code generation and restart IDE.
make l10n # Generate localization files
make l10n-check # Check for missing translations
make l10n-sort # Sort ARB files alphabetically