As Flutter applications scale beyond a single market, language support becomes a critical requirement. A well-designed app should feel natural to users regardless of their locale, automatically adapting to their language preferences while still giving them control.
This article provides a comprehensive, production-focused guide to supporting multiple languages in a Flutter application using Flutter’s localization system, the intl package, and Bloc for state management. We’ll support English, French, and Spanish, implement automatic language detection, and allow users to manually switch languages from settings, while also exploring the use of AI to automate text translations.
Table of Contents
Prerequisites
Before proceeding, you should be comfortable with the following concepts:
Dart programming language: variables, classes, functions, and null safety
Flutter fundamentals: widgets,
BuildContext, and widget treesState management basics: familiarity with Bloc or similar patterns
Terminal usage: running Flutter CLI commands
If you have prior experience working with Flutter widgets and basic app architecture, you are well prepared to follow along.
Why Localization Matters in Flutter Applications
Localization (often abbreviated as l10n) is the process of adapting an application for different languages and regions, going beyond simple text translation to influence accessibility, user trust, and overall usability. From a technical perspective, localization introduces several challenges: text must be dynamically resolved at runtime, the UI must update instantly when the language changes, language preferences must persist across sessions, and device locale detection must gracefully fall back when a language is unsupported.
Flutter’s localization framework, when combined with intl and Bloc, solves these challenges cleanly and predictably.
Flutter Localization Architecture Overview
Flutter localization is built around three key ideas:
ARB files as the source of truth for translated strings
Code generation to provide type-safe access to translations
Locale-driven rebuilds of the widget tree
At runtime, the active Locale determines which translation file is used. When the locale changes, Flutter automatically rebuilds dependent widgets.
How to Set Up Dependencies
Add the required dependencies to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.2
flutter_bloc: ^8.1.3
arb_translate: ^1.1.0
Enable localization code generation:
flutter:
generate: true
This instructs Flutter to generate localization classes from ARB files.
How to Define Supported Languages
For this guide, the application will support:
English (
en)French (
fr)Spanish (
es)
These locales will be declared centrally and used throughout the app.
How to Add Localized Text with ARB Files
Flutter uses Application Resource Bundle (ARB) files to store localized strings. Each supported language has its own ARB file.
English – app_en.arb
{
"@@locale": "en",
"enter_email_address_to_reset": "Enter your email address to reset"
}
French – app_fr.arb
{
"@@locale": "fr",
"enter_email_address_to_reset": "Entrez votre adresse e-mail pour réinitialiser"
}
Spanish – app_es.arb
{
"@@locale": "es",
"enter_email_address_to_reset": "Ingrese su dirección de correo electrónico para restablecer"
}
Each key must be identical across files. Only the values change per language.
How to Generate Localization Code
Run the following command in your terminal:
flutter gen-l10n
Flutter generates a strongly typed localization class, typically located at:
.dart_tool/flutter_gen/gen_l10n/app_localizations.dart
This file exposes getters such as:
AppLocalizations.of(context)!.enter_email_address_to_reset
How to Configure MaterialApp for Localization
The MaterialApp widget must be configured with localization delegates and supported locales:
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('fr'),
Locale('es'),
],
locale: state.locale,
home: const MyHomePage(),
)
The locale property is controlled by Bloc, allowing dynamic updates at runtime.
Auto-Detecting the User’s Device Language
Flutter exposes the device locale via PlatformDispatcher. We can use this to automatically select the most appropriate supported language.
void detectLanguageAndSet() {
Locale deviceLocale = PlatformDispatcher.instance.locale;
Locale selectedLocale = AppLocalizations.supportedLocales.firstWhere(
(supported) => supported.languageCode == deviceLocale.languageCode,
orElse: () => const Locale('en'),
);
print('Using Locale: ${selectedLocale.languageCode}');
GlobalConfig.storageService.setStringValue(
AppStrings.DETECTED_LANGUAGE,
selectedLocale.languageCode,
);
context.read<AppLocalizationBloc>().add(
SetLocale(locale: selectedLocale),
);
}
This approach reads the device language, matches it against supported locales, falls back to English when the language is unsupported, persists the detected language, and updates the UI instantly.
How to Manage Localization with Bloc
Bloc provides a predictable and testable way to manage application-wide locale changes.
Localization State
class AppLocalizationState {
final Locale locale;
const AppLocalizationState(this.locale);
}
Localization Event
abstract class AppLocalizationEvent {}
class SetLocale extends AppLocalizationEvent {
final Locale locale;
SetLocale({required this.locale});
}
Localization Bloc
class AppLocalizationBloc
extends Bloc<AppLocalizationEvent, AppLocalizationState> {
AppLocalizationBloc()
: super(const AppLocalizationState(Locale('en'))) {
on<SetLocale>((event, emit) {
emit(AppLocalizationState(event.locale));
});
}
}
The AppLocalizationBloc manages the app’s language state. It starts with English (Locale('en')) as the default, and when it receives a SetLocale event, it updates the state to the new locale provided in the event, causing the app’s UI to switch to that language. Whenever SetLocale is dispatched, the entire app rebuilds using the new locale.
How to Display Localized Text in Widgets
Once localization is configured, using translated text is straightforward:
Text(
AppLocalizations.of(context)!.enter_email_address_to_reset,
style: getRegularStyle(
color: Colors.white,
fontSize: FontSize.s16,
),
)
AppLocalizations.of(context)!.enter_email_address_to_reset retrieves the localized string enter_email_address_to_reset for the current app locale from the generated localization resources. The correct translation is resolved automatically based on the active locale.
Language Switching from Settings
Users should always be able to override automatic language detection.
ListTile(
title: const Text('French'),
onTap: () {
context.read<AppLocalizationBloc>().add(
SetLocale(locale: const Locale('fr')),
);
},
)
This ListTile displays the text "French", and when tapped, it triggers the AppLocalizationBloc to change the app’s locale to French ('fr') by dispatching a SetLocale event and it persists the selected language so it can be restored on the next app launch.
How to Add Parameters to Localized Strings
Real-world applications rarely display static text. Messages often include dynamic values such as user names, counts, dates, or prices. Flutter’s localization system, powered by intl, supports parameterized (interpolated) strings in a type-safe way.
Where Parameters Are Defined
Parameters are defined inside ARB files alongside the localized string itself, with each parameterized message consisting of the message string containing placeholders and a corresponding metadata entry that describes those placeholders.
Example: Parameterized Text
Suppose we want to display a greeting message that includes a user’s name.
English – app_en.arb
{
"@@locale": "en",
"greetingMessage": "Hello {username}!",
"@greetingMessage": {
"description": "Greeting message shown on the home screen",
"placeholders": {
"username": {
"type": "String"
}
}
}
}
This defines a parameterized localized message for English, indicated by "@@locale": "en". The "greetingMessage" key contains the string "Hello {username}!", where {username} is a placeholder that will be dynamically replaced with the user’s name at runtime. The "@greetingMessage" entry provides metadata for the message, including a description that explains the string is shown on the home screen, and a "placeholders" section that specifies "username" is of type String. When the app runs, this structure allows the message to display dynamically—for example, if the username is "Alice", the message would appear as "Hello Alice!".
French – app_fr.arb
{
"@@locale": "fr",
"greetingMessage": "Bonjour {username} !"
}
Spanish – app_es.arb
{
"@@locale": "es",
"greetingMessage": "¡Hola {username}!"
}
The placeholder name ({username}) must be identical across all ARB files.
Generated Dart API
After running:
flutter gen-l10n
Flutter generates a strongly typed method instead of a simple getter:
String greetingMessage(String username)
This prevents runtime errors and ensures compile-time safety.
How to Use Parameterized Strings in Widgets
Text(
AppLocalizations.of(context)!.greetingMessage('Tony'),
)
If the locale is set to French, the output becomes:
Bonjour Tony !
Pluralization and Quantities
Another common localization requirement is pluralization. Languages differ significantly in how they express quantities, and hardcoding plural logic in Dart quickly becomes error-prone.
Defining Plural Messages in ARB
{
"itemsCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemsCount": {
"description": "Displays the number of items",
"placeholders": {
"count": {
"type": "int"
}
}
}
}
This defines a pluralized message for itemsCount. The string {count, plural, =0{No items} =1{1 item} other{{count} items}} dynamically changes based on the value of count: it shows "No items" when count is 0, "1 item" when count is 1, and "{count} items" for all other values. The metadata entry "@itemsCount" provides a description and specifies that the placeholder count is of type int.
Each language can define its own plural rules while sharing the same key.
Using Pluralized Messages
Text(
AppLocalizations.of(context)!.itemsCount(3),
)
Flutter automatically applies the correct plural form based on the active locale.
How to Format Dates, Numbers, and Currency
The intl package also provides locale-aware formatting utilities. These should be used in combination with localized strings, not as replacements.
Date Formatting Example
final formattedDate = DateFormat.yMMMMd(
Localizations.localeOf(context).toString(),
).format(DateTime.now());
Text(
AppLocalizations.of(context)!.lastLoginDate(formattedDate),
)
This ensures that both language and formatting rules align with the user’s locale.
Localization Data Flow
Localization is handled as an explicit data flow, with locale resolution modeled as application state rather than a static configuration passed into MaterialApp.
The process starts with the device locale, obtained from the platform layer at startup. This value represents the system’s preferred language and region but is not applied directly to the UI.
Instead, it flows through a detectLanguageAndSet step responsible for applying application-specific rules. This layer typically handles locale normalization and fallback logic, such as mapping unsupported locales to supported ones, restoring a user-selected language from persistent storage, or enforcing product constraints around available translations.
The resolved locale is then emitted into a Localization Bloc, which acts as the single source of truth for localization state. By centralizing locale management, the application can support runtime language changes, ensure predictable rebuilds, and keep localization logic decoupled from both the widget tree and platform APIs.
The Bloc feeds into the locale property of MaterialApp, which is the integration point with Flutter’s localization system. Updating this value triggers a rebuild of the Localizations scope and causes all dependent widgets to resolve strings for the active locale.
At the edge of the system, localized widgets consume the generated localization classes produced by flutter gen-l10n. These widgets remain agnostic to how the locale was selected or updated. They simply react to the localization context provided by the framework.
This architecture cleanly separates:
Locale detection
Business logic and state management
Framework-level localization
UI rendering
As a result, localization behavior remains explicit, maintainable, and compatible with automated translation workflows and CI-driven localization updates.

Common Pitfalls and How to Avoid Them
Avoid manual string concatenation. For example, do not use
'Hello ' + name. You should rely on localized templates instead.Never hardcode plural logic in Dart. Always use
intl’s pluralization features to handle different languages correctly.Avoid locale-specific formatting outside
intlutilities. Dates, numbers, and currencies should be formatted using the proper localization tools.Always regenerate localization files after updating ARB files. This ensures the app reflects all the latest translations.
How to Automate Translations with AI
In Flutter applications that rely on ARB files for localization, translation maintenance becomes increasingly costly as the application grows. Each new message must be manually propagated across locale files, often resulting in missing keys, inconsistent phrasing, or delayed updates. This problem is amplified in projects that do not use a Translation Management System (TMS) and instead keep ARB files directly in the repository.
While many TMS platforms have begun adding AI-assisted translation features, not all projects use a TMS at all, particularly small teams, internal tools, or personal projects. In these cases, developers frequently resort to copying strings into AI chat tools and pasting results back into ARB files, which is inefficient and difficult to scale.
To address this workflow gap, Leen Code published arb_translate package, a Dart-based CLI tool that automates missing ARB translations using large language models.
Design Approach
The model behind arb_translate aligns with Flutter’s existing localization pipeline rather than replacing it:
English ARB files remain the source of truth
Only missing keys are translated
Output is written back as standard ARB files
flutter gen-l10nis still responsible for code generation
This design makes the tool suitable for both local development and CI usage, without introducing new runtime dependencies or localization abstractions.
At a high level, the flow is:
Parse the base (typically English) ARB file
Identify missing keys in target locale ARB files
Send key–value pairs to an LLM via API
Receive translated strings
Update or generate locale-specific ARB files
Run
flutter gen-l10nto regenerate localized resources
Gemini-Based Setup
To use Gemini for ARB translation:
Generate a Gemini API key
https://ai.google.dev/tutorials/setup
Install the CLI:
dart pub global activate arb_translate
- Export the API key:
export ARB_TRANSLATE_API_KEY=your-api-key
- Run the tool from the Flutter project root:
arb_translate
The tool scans existing ARB files, generates missing translations, and writes them back to disk.
OpenAI/ChatGPT Support
As of version 1.0.0, arb_translate also supports OpenAI ChatGPT models. This allows teams to standardize on OpenAI infrastructure or switch providers without changing their localization workflow.
Generate an OpenAI API key
https://platform.openai.com/api-keys
Install the tool:
dart pub global activate arb_translate
- Export the API key:
export ARB_TRANSLATE_API_KEY=your-api-key
- Select OpenAI as the provider:
Via l10n.yaml:
arb-translate-model-provider: open-ai
Or via CLI:
arb_translate --model-provider open-ai
- Execute:
arb_translate
Practical Use Cases
This approach is not intended to replace professional translation or review workflows. Instead, it serves as a deterministic automation layer that:
Eliminates manual copy-paste workflows
Keeps ARB files structurally consistent
Enables translation generation in CI
Allows downstream review in a TMS if required
For content-heavy Flutter applications or teams without a dedicated localization platform, this provides a pragmatic and maintainable solution.
Best Practices and Considerations
Always define a fallback locale to ensure the app remains usable.
Avoid hardcoding user-facing strings; rely on localized resources.
Use semantic and stable ARB keys for maintainability.
Persist user language preferences to provide a consistent experience.
Test your app with long translations and multiple locales to catch layout or UI issues.
Conclusion
Localization is a foundational requirement for modern Flutter applications. By combining Flutter’s built-in localization framework, the intl package, and Bloc for state management, you gain a robust and scalable solution.
With automatic device language detection, runtime switching, and clean architecture, your application becomes globally accessible without sacrificing maintainability.
References
Here are official links you can use as references for Flutter localization:
Flutter Internationalization Guide – Official Flutter guide on how to internationalize your app:
https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalizationDart
intlPackage Documentation – API reference for theintllibrary used for formatting and localization utilities:
https://api.flutter.dev/flutter/package-intl_intl/index.htmlFlutter
flutter_localizationsAPI – API docs for theflutter_localizationslibrary that provides localized strings and resources for Flutter widgets:
https://api.flutter.dev/flutter/flutter_localizations/Flutter App Localization with AI (LeanCode) – A guide on speeding up Flutter localization using AI and tools like Gemini or ChatGPT, including details on the
arb_translatepackage.
https://leancode.co/blog/flutter-app-localization-with-aiarb_translatepackage (pub.dev) – A tool for automating ARB file translations in Flutter:
https://pub.dev/packages/arb_translate