Internationalization (i18n) and localization (l10n) best practices for Flutter applications using Flutter's built-in localization system with ARB files as the single source of truth.
Core Standards
Apply these standards to all internationalization work:
- Never hardcode user-facing strings — all text must go through the l10n system
- Use Flutter's built-in localization system —
flutter_localizations+intl, never third-party i18n libraries - ARB files are the single source of truth for all translations
BuildContextextension for cleaner l10n access — usecontext.l10ninstead ofAppLocalizations.of(context)- Pass localized strings as parameters to reusable widgets — never couple shared widgets directly to
AppLocalizations - Use
EdgeInsetsDirectional(start/end) instead ofEdgeInsets(left/right) — ensures correct layout in RTL languages - Handle RTL layout properly — use directional widgets for padding, positioning, and alignment
- Implement i18n early — even if only one language is planned initially, the overhead is small and the long-term benefit is significant
- Dart 3.13 primary constructors — on a Dart 3.13+ baseline, declare a reusable widget's localized-string fields as primary-constructor declaring parameters (
class const ConfirmDialog({required final String title, super.key}) extends StatelessWidget) rather thanthis.field; keep the classic form only below 3.13
Setup Pipeline and ARB File Format
Add flutter_localizations and intl as dependencies, enable generate: true in pubspec.yaml, configure l10n.yaml, create ARB files in lib/l10n/arb/, run flutter gen-l10n, and wire up MaterialApp with localizationsDelegates and supportedLocales. ARB files support simple strings, placeholders, and ICU plural syntax.
BuildContext Extension
Create an extension for ergonomic l10n access throughout the codebase:
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
Usage:
// Preferred
Text(context.l10n.helloWorld);
// Avoid
Text(AppLocalizations.of(context).helloWorld);
Reusable Widget Strategy
Shared widgets that live in separate packages should not depend on AppLocalizations directly. Instead, pass localized strings as constructor parameters:
When someone asks to add AppLocalizations to a shared package, decline and say why — the package would carry its own translations and every consuming app would be locked to them — then rewrite their widget with the label as a final String constructor parameter and show the call site supplying context.l10n. Give both as Dart code; describing the change in prose leaves the caller to guess the signature.
// Shared widget — no l10n dependency
class const ConfirmDialog({
required final String title,
required final String message,
required final String confirmLabel,
required final String cancelLabel,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(cancelLabel)),
FilledButton(onPressed: () => Navigator.pop(context, true), child: Text(confirmLabel)),
],
);
}
}
// App-level usage — passes localized strings
showDialog<bool>(
context: context,
builder: (_) => ConfirmDialog(
title: context.l10n.deleteTitle,
message: context.l10n.deleteMessage,
confirmLabel: context.l10n.confirm,
cancelLabel: context.l10n.cancel,
),
);
Text Directionality
Use EdgeInsetsDirectional (start/end) instead of EdgeInsets (left/right) for all padding and margins. Use directional widget variants (PositionedDirectional, AlignDirectional, BorderDirectional) for RTL-aware layouts. Icons mirror automatically in RTL; images require matchTextDirection: true.
Backend Considerations
Store backend content with per-locale translations and require clients to transmit the user's locale. For error messages, map HTTP status codes or custom backend error constants to l10n keys on the frontend.
Common Patterns
Adding a New Locale
- Create
app_<locale>.arbinlib/l10n/arb/(e.g.,app_fr.arb) - Add translations for all keys from the template ARB file
- Run
flutter gen-l10n - The new locale is automatically available through
AppLocalizations.supportedLocales
Adding a New String
- Add the key-value pair to the template ARB file (
app_en.arb) - Add the
@keymetadata with description and placeholders if needed - Add translations in all other ARB files
- Run
flutter gen-l10n - Use via
context.l10n.newKey
Pluralization
- Define the plural string in the template ARB file using ICU message syntax
- Provide placeholder metadata with
"type": "int" - Add plural forms in all locale ARB files
- Use via
context.l10n.itemCount(items.length)
Additional Resources
- references/setup.md — full step-by-step setup pipeline and ARB file format examples
- references/directionality.md — visual vs directional widgets, icon/image mirroring, Material bidirectionality standards
- references/backend.md — multi-language content storage and error message localization