Flutter meets Igbo:


Multilingual Software Applications: Why We Need Localization.
Localization in mobile software development refers to the process of adapting apps to support multiple languages and cultural settings. It emerged alongside the rapid growth of the mobile app ecosystem in the early 2000s. Initially, apps were primarily focused on a global audience, but the need for localization became evident as users from diverse cultural backgrounds began to adopt mobile devices.
Project Setup
P.S. I assume the reader of this article is familiar with the Flutter framework. 😅
A public repository for the full implementation is available on GitHub for you to follow along.
Installing 3rd-party plugins
The first step is creating a new Flutter project. Run “flutter create <DIRECTORY>” in the command line,
flutter create <DIRECTORY>
Below is a list of the 3rd-party plugins used in this project:
Cached_network_image: “A flutter library to show images from the internet and keep them in the cache directory.”
Easy_localization: “A reactive caching and data-binding framework.”
Faker: “Faker is a Python package that generates fake data for you. Whether you need to bootstrap your database, create good-looking XML documents, fill in your persistence to stress test it, or anonymize data taken from a production service, Faker is for you.”
Flutter_widget_from_html: “Flutter package to render HTML as widgets that supports hyperlink, image, audio, video, iframe, and 70+ other tags.”
Intl: “Provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text.”
Carousel_slider: “A carousel slider widget”.
dependencies:
cached_network_image: ^3.4.1
carousel_slider: ^5.0.0
easy_localization: ^3.0.7+1
faker: ^2.2.0
flutter:
sdk: flutter
flutter_widget_from_html: ^0.16.0
intl: ^0.19.0
…
Add “Easy_localization” dependency.
One can also configure localizations using other plugins on pub.dev, such as:
And many more! But for the sake of this project, I have a personal liking for Easy_localization because:
It is always maintained
It has comprehensive documentation and is easy to understand for any level of development
It is also a common choice, as it has the most stats/scores by developers amongst the others on pub (LIKES = 3.59k, PUB POINTS = 150/160, DOWNLOADS = 74.6k) as at 11th June, 2025
It also supports loading translation files with extensions such as JSON, XML, YAML, and CSV
It supports plural, gender, nesting, and RTL locales (like Arabic)
It also promotes fallback locale keys redirection; that means if the selected locale isn’t in the list, it falls back to a set locale.
To install easy_localization in the project, you should start by reading the documentation on pub.dev: Easy_localization.
Add the
easy_localization
dependency on your “pubspec.yaml” file: dependencies:dependencies: flutter: sdk: flutter easy_localization: ^3.0.7
Run flutter pub get
…
In the main.dart:
Configure the project by initializing it above the runApp() method inside the main(), and wrap the MyApp() root widget with the EasyLocalization widgets.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
EasyLocalization(
supportedLocales: const [Locale('en', 'US'), Locale('es', 'ES'), Locale('ig', 'NG')],
saveLocale: true,
path: 'assets/l10n',
fallbackLocale: const Locale('en', 'US'),
child: const MyApp(),
),
);
}
The EasyLocalization Widget has a couple of parameters with some required*:
supportedLocales
*: List of supported locales.path
*: Path to your folder with localization files.savedLocale
: Save locale in device storage.fallbackLocale
: Returns the locale when the locale is not in the list of supportedLocales.startLocale
: Overrides device locale.assetLoader
: Class loader for localization files. You can use custom loaders from Easy Localization Loader or create your class.errorWidget
: Shows a custom error widget when an error occurs.
On the pubspec.yaml, adding the path of the translation files to “assets”
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/l10n/
In the MaterialApp() widget, we will configure the localizationDelegates, supportedLocales, and locale. Since we are using the easy_localization plugin, we can get these parameters via its extension to the build context, courtesy of the plugin.
MaterialApp(
// ...
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
// ...
);
Looking at the Easy_localization codebase, we discover that the context.localizationDelegates
is a getter for the MaterialLocalization classes, so we don’t need to pass the Flutter material localizations to supported locales in the MaterialApp Widget.
class _EasyLocalizationProvider extends InheritedWidget {
final EasyLocalization parent;
final EasyLocalizationController _localeState;
final Locale? currentLocale;
final _EasyLocalizationDelegate delegate;
/// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
///
/// ```dart
/// delegates = [
/// delegate
/// GlobalMaterialLocalizations.delegate,
/// GlobalWidgetsLocalizations.delegate,
/// GlobalCupertinoLocalizations.delegate
/// ],
///
List get delegates => [ delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ];
/// Get List of supported locales List get supportedLocales => parent.supportedLocales;
...
}
**Please Note ⚠️** : For translation to work on **iOS,** you need to add supported locales to `ios/Runner/Info.plist` as shown below:
```swift
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>es</string>
<string>ig</string>
</array>
To start the translations, we create a class to hold the keys of the text to be translated into any language.
abstract class TextConstant {
static const appTitle = 'appTitle';
static const createYourAcct = 'createYourAcct';
static const haveAnAcct = "haveAnAcct";
static const login = 'login';
static const firstName = 'firstName';
static const enterFullNameAsWritten = 'enterFullNameAsWritten';
static const lastName = 'lastName';
static const userName = 'userName';
static const emailAddress = 'emailAddress';
static const password = 'password';
static const emailHint = 'emailHint';
static const confirmPassword = 'confirmPassword';
static const createAccount = 'createAccount';
static const passwordMustBe = 'passwordMustBe';
static const loginToYourAccount = 'loginToYourAccount';
static const dontHaveAnAcct = "dontHaveAnAcct";
// ... // ADD AS MUCH YOU WANT
}
Create the “L10n folder”
The “L10n” folder contains the translation files (en-US, es-ES, ig-NG), and for the sake of this project, I will be using the JSON file type.
According to the Easy_localization
documentation on pub.dev, the translation files can be named in two ways:
only language code + extension: such as (en.json, ig.json, es.json)
Language code – country code + extension: such as (en-US.json, es-ES.json, ig-NG.json)
We create the JSON files:
// en-US.json, The english file
{
"appTitle": "Igbo Flutter Locale Demo",
"weAreGladYouAreHere": "We are glad you are here",
"createYourAcct" : "Create your account",
"haveAnAcct" : "Have an account?",
"login" : "Login",
"firstName" : "First Name",
"enterFullNameAsWritten" : "enter your full name as its written on your id card",
"lastName" : "Last Name",
"userName" : "Username",
"emailAddress" : "Email address"
// ...
}
//es-ES.json, The Spanish file
{
"appTitle": "Demostración de localización Flutter Igbo",
"confirm": "Confirmar",
"confirmPassword": "confirmar Contraseña",
"createAccount": "Crear una cuenta",
"createYourAcct": "Crea tu cuenta",
"dontHaveAnAcct": "¿No tienes una cuenta?",
"emailAddress": "Dirección de correo electrónico"
// ...
}
//ig-NG.json. The Igbo file
{
"appTitle": "Ngosipụta Asụsụ Igbo Flutter",
"alreadyHaveAnAcct": "Ị nweelarị akaụntụ?",
"signUp": "Debanye aha",
"createYourAcct" : "Mepụta akaụntụ gị",
"haveAnAcct" : "Ị nwere akaụntụ?",
"login" : "Banye",
"firstName" : "Aha mbụ",
"enterFullNameAsWritten" : "Tinye aha gị zuru oke dịka edere ya na kaadị njirimara gị.",
"lastName" : "Aha nna",
"userName" : "Aha njirimara",
"emailAddress" : "Adreesị ozi-e"
// ...
}
Create Locale Switch
I created an enum to contain the Flag
, Title
and Locale
for every language. This would be helpful in the locale switch.
enum L10nEnum {
enUS('🇺🇸', 'English', Locale('en', 'US')),
esES('🇪🇸', 'Spanish', Locale('es', 'ES')),
igNG('🇳🇬', 'Igbo', Locale('ig', 'NG'));
const L10nEnum(this.flag, this.lang, this.locale);
final String flag;
final String lang;
final Locale locale;
static L10nEnum fromLocale(Locale locale) {
return L10nEnum.values.firstWhere(
(e) => e.locale.languageCode == locale.languageCode && e.locale.countryCode == locale.countryCode,
orElse: () => L10nEnum.enUS,
);
}
}
In the profile_screen
, I created a Widget that allows me to change between locales.
showModalBottomSheet(
context: context,
isDismissible: false,
elevation: 4,
backgroundColor: themeContext.scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)),
side: BorderSide(color: themeContext.primaryColor, width: 0.2),
),
builder: (context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: context.supportedLocales.map((locale) {
final L10nEnum localeEnum = L10nEnum.fromLocale(locale);
final flag = localeEnum.flag;
final lang = localeEnum.lang;
return ListTile(
onTap: () {
context.setLocale(locale);
chatController.setL10nEnum(localeEnum, context: context);
},
contentPadding: const EdgeInsets.symmetric(vertical: 5),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: themeContext.scaffoldBackgroundColor,
border: Border.all(color: themeContext.primaryColor),
borderRadius: BorderRadius.circular(10),
),
child: Text(flag, textScaleFactor: 1.6),
),
title: Text(lang, textAlign: TextAlign.start, textScaleFactor: 1.3),
);
}).toList(),
),
)
Using the showModalBottomSheet
, a standard Material Design widget, to present users with a list of supported languages or regional formats. This modal interface slides up from the bottom of the screen, offering an intuitive way for users to view and potentially select their preferred locale. To streamline the handling and representation of these locales within the codebase, the enum L10nEnum
is employed. This enum is designed to correspond to the Locale
objects retrieved from context.supportedLocales
. This approach facilitates more robust and type-safe logic when working with different localizations, for instance, when displaying localized names for each language or managing locale-specific resources and behaviours within the application.
context.setLocale(locale)
: This is a method provided by the plugin; it saves the selected locale and triggers the translations.
chatController.setL10nEnum(localeEnum, context: context)
: It is a ChangeNotifier class I created in the ChatController file to trigger translation in the Chat_View. You can view the contents of the chatController class here on GitHub.
On the Text Widget
In reviewing localization strategies, I will cover the various methods outlined in the documentation. However, for optimal reliability and to prevent common pitfalls, my preferred approach is to utilise statically defined string constants as translation keys. This practice significantly reduces the risk of typographical errors, a frequent cause of failed translations.
//METHOD 1: Wrapping the translation with context.tr()
Text(context.tr('settings'))
//METHOD 2: using the tr() without [Context] as a static function
Text('settings').tr() //Text widget
var settings = tr('settings');// As String; tr() returns a String
Text(settings);
//METHOD 3: MY Preferred approach
Text(TextConstant.settings.tr(),)
Please Note ⚠️ : The documentation warns against Method 2, saying it’s not recommended in build methods because the widget won't rebuild when the language changes.
The tr() method
The tr() method is a static method from the easy_localization plugin that returns a String. It also takes other arguments, such as args
, namedArgs
, and gender
.
Continue with the example, and I will explain the arguments above. We will edit any of the JSON files that contain the translations. In this case, I will edit the en-US.json.
{
//...
//ARGS 1 & 2
"settings" : "{} Settings"
"settings" : "{} Settings is {}"
//NAMED_ARGS
"settings" : "{name} Settings"
//GENDER
"settings" : {
"male": "His Settings",
"female": "Her Settings",
"other": "Settings"
}
//...
}
View Results
- FOR ARGS
//ARGS 1
Text(
TextConstant.settings.tr(args: ['My']), // OUTPUT: "My Settings"
),
//ARGS 2
Text(
TextConstant.settings.tr(args: ['My', 'Open']), // OUTPUT: "My Settings is Open"
),
- FOR NAMED ARGS
Text(
TextConstant.settings.tr(namedArgs: {'name': 'Marcel'}), // OUTPUT: "Marcel Settings"
),
- GENDER
Text(
TextConstant.settings.tr(gender: 'male'), // OUTPUT: "His Settings"
),
Text(
TextConstant.settings.tr(gender: 'female'), // OUTPUT: "Her Settings"
),
Observation
When we switched between locales, it worked until we switched to “Igbo” language, and a widget exception occurred, saying:
════════════════Exception caught by the widgets library ══════════════
No MaterialLocalizations found.
At its core, this error means a widget from Flutter's Material library (like TextField
, AlertDialog
, or the back button's tooltip) tried to display a piece of text, but couldn't find the translation for the app's current language.The MaterialLocalizations
class is responsible for providing these framework-level strings. To do this, MaterialApp
needs a "delegate" that knows how to load these strings for a given locale.
To fix the “No MaterialLocalization found” Error:
- We create the translations class, which is similar to our translations JSON files, but here we extend the class to
DefaultMaterialLocalizations
and translate to the Igbo language.
class IgboMaterialLocalizations extends DefaultMaterialLocalizations {
@override
String get openAppDrawerTooltip => 'Mepee drawer nnabata';
@override
String get backButtonTooltip => 'Azụ';
@override
String get closeButtonTooltip => 'Mechie';
@override
String get deleteButtonTooltip => 'Hichapụ';
@override
String get nextMonthTooltip => 'Ọnwa na-esote';
@override
String get previousMonthTooltip => 'Ọnwa gara aga';
@override
String get nextPageTooltip => 'Ibe na-esote';
@override
String get previousPageTooltip => 'Ibe gara aga';
@override
String get showMenuTooltip => 'Gosi menu';
@override
String get cancelButtonLabel => 'Kagbuo';
static const LocalizationsDelegate<MaterialLocalizations> delegate = _IgboMaterialLocalizationsDelegate();
//... AND MANY OTHHER OVERRIDES
}
The DefaultMaterialLocalizations
is a concrete class provided by Flutter that contains all the required strings in English. By extending it, we only need to @override the strings we want to translate into Igbo. If you happen to miss one, it will safely fall back to the English version from the parent class, rather than causing an error.
- We create the delegate, which tells Flutter how and when to load our
IgboMaterialLocalizations
class.MaterialApp
maintains a list of these delegates and consults them whenever the locale changes.
class _IgboMaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
const _IgboMaterialLocalizationsDelegate();
@override
bool isSupported(Locale locale) => locale.languageCode == 'ig';
@override
Future<MaterialLocalizations> load(Locale locale) async => IgboMaterialLocalizations();
@override
bool shouldReload(_IgboMaterialLocalizationsDelegate old) => false;
}
This class has three essential methods:
isSupported(Locale locale)
: This is the first method Flutter calls. It asks the delegate, "Can you handle this language?" Here, it simply checks if the language code is'ig'
. If it returnstrue
Flutter proceeds to the next step.load(Locale locale)
: SinceisSupported
returnedtrue
Flutter now callsload
to get the actual translation object. This method's only job is to create and return an instance of ourIgboMaterialLocalizations
class.shouldReload(...)
: This is an optimization. It asks if the translations need to be reloaded when the widget tree rebuilds. Since our Igbo strings are hard-coded and won't change at runtime, I set it tofalse
.
Finally, I expose a static instance of the delegate for easy access, which can be found in my MaterialApp
like IgboMaterialLocalizations.delegate
.
- Repeating the Pattern for
Widgets
andCupertino
I have identified that Material
isn't the only widget set. The same pattern is repeated for:
IgboWidgetsLocalizations
: For basic, non-styled widgets (handles things like text direction).IgboCupertinoLocalizations
: For iOS-style widgets.
By providing delegates for all three, we ensure that our app is fully and correctly localized, regardless of the Flutter widgets we use.
Our main.dart
now looks like this:
MaterialApp(
//...
localizationsDelegates: [
IgboCupertinoLocalizations.delegate,
IgboMaterialLocalizations.delegate,
IgboWidgetsLocalizations.delegate,
...context.localizationDelegates,
],
supportedLocales: context.supportedLocales,
locale: context.locale,
//...
);
Voila ✅ 🥳🥳, Everything works well now!
Conclusion
This article provides a comprehensive guide to setting up a Flutter project with Easy Localization integration, highlighting essential steps such as creating a new Flutter project, installing third-party plugins like Shared_Preferences
, faker
, and flutter_widget_from_html
, and configuring Android and iOS platforms for locale functionality.
In conclusion, effectively localizing a Flutter application is paramount for reaching a global audience, and the easy_localization package emerges as a powerful and developer-friendly solution to achieve this. As we've explored, its straightforward setup, coupled with robust features like automatic locale saving, fallback mechanisms, and support for various translation file formats, significantly streamlines the internationalization workflow.
👉 Ready to get started? Check out the full implementation on GitHub(Flutter_meets_igbo) and bring your maps to life!
References
Flutter Meets Igbo Github Repository
Mobile app localization: Importance and steps
Visiting the History of Localization to Understand the Future
Subscribe to my newsletter
Read articles from Nkpozi Marcel Kelechi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
