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:

  1. Cached_network_image: “A flutter library to show images from the internet and keep them in the cache directory.

  2. Easy_localization: “A reactive caching and data-binding framework.”

  3. 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.

  4. Flutter_widget_from_html: “Flutter package to render HTML as widgets that supports hyperlink, image, audio, video, iframe, and 70+ other tags.

  5. Intl: “Provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text.

  6. 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:

  1. only language code + extension: such as (en.json, ig.json, es.json)

  2. 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 L10nEnumis 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

  1. 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"
),
  1. FOR NAMED ARGS
Text(
TextConstant.settings.tr(namedArgs: {'name': 'Marcel'}), // OUTPUT: "Marcel Settings"
),
  1. 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:

  1. 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.

  1. 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 returns trueFlutter proceeds to the next step.

  • load(Locale locale): Since isSupported returned trueFlutter now calls load to get the actual translation object. This method's only job is to create and return an instance of our IgboMaterialLocalizations 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 to false.

Finally, I expose a static instance of the delegate for easy access, which can be found in my MaterialApp like IgboMaterialLocalizations.delegate.

  1. Repeating the Pattern for Widgets and Cupertino

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

Easy Localization (pub.dev)

faker (pub.dev)

0
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

Nkpozi Marcel Kelechi
Nkpozi Marcel Kelechi