Localization in multi-module project with intl_utils

Renat Sarymsakov
Dev Artel
Published in
4 min readAug 3, 2023

Disclaimer

You will find this article useful if you rely on intl_utils. For new projects, we recommend using a modern localization solution provided by Flutter framework.

Intro

Any mobile application that aims at the global market requires localization. Localization is carried out by keeping strings separate from code via resource files (e.g., ARB files). As the complexity of the application increases, you have to divide the codebase into several modules. Each module contains a set of classes and resources, which are tailored to perform a single function. This is where you may encounter a problem in the modularization of string resources.

What are the benefits of such architecture?

  • You can extract your UI components into a separate library and use it in your app as well as in the app’s storybook
  • You can move features into feature modules to manage visibility of feature classes
  • You are sure that all your dependencies are isolated from non-compatible environments

When localizing an app in Flutter, you utilize localization delegates. These are generated from string resource files during the construction of a MaterialApp, as follows:

final localizationDelegates = <LocalizationsDelegate<Object>>[
// app-specific localization delegate(s) here
];

// ...

const MaterialApp(
localizationsDelegates: <LocalizationsDelegate<Object>>[
...localizationDelegates,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: <Locale>[
Locale('en', 'US'), // English
Locale('he', 'IL'), // Hebrew
// ... other locales the app supports
],
// ...
)

However, if you try to add multiple delegates with the same locale (in a multilingual application), derived from different packages, only the first batch of strings will be added. It works fine with the initial language of the app. But we face a problem once we change a language either in the app or in the system preferences.

Suppose we have a main app module and a module containing widgets:

// ui.dart

class Greeting extends StatelessWidget {
const Greeting({super.key});
@override
Widget build(BuildContext context) => Text(
S.of(context).helloA,
);
}
// main.dart

class Demo extends StatelessWidget {
const Demo({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(S.of(context).title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const ui.Greeting(),
TextButton(
onPressed: () => LocaleNotifier.instance.switchLocale(),
child: Text(S.of(context).changeLanguage),
),
],
),
),
);
}

Note that each of files uses their own S.of(context). This works as expected when you launch the app. But if you change language, you notice that only a title in the AppBar changes.

Let’s take a deep dive

The cause can be traced back to the internals of the intl_utils package.

Within this package, the method addLocale in the message lookup class ignores the ones that have been added previously:

@override
void addLocale(String localeName, Function findLocale) {
if (!localeExists(localeName)) {
// ...
}
// ...
}

However, this issue does not affect the stacking of **GlobalMaterialLocalizations** and **GlobalWidgetsLocalizations** messages.

Why do Global*Localization delegates work?

As of today, the mechanism for localizing Material widgets is kept completely separate from user-defined localizations. Global*Localization delegates are accessible via context. For example, let's take a look at the build method of AboutListTile:

@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
return ListTile(
leading: icon,
title: child ?? Text(MaterialLocalizations.of(context).aboutListTileTitle(
applicationName ?? _defaultApplicationName(context),
)),
dense: dense,
onTap: () {
showAboutDialog(
context: context,
applicationName: applicationName,
applicationVersion: applicationVersion,
applicationIcon: applicationIcon,
applicationLegalese: applicationLegalese,
children: aboutBoxChildren,
);
},
);
}

As you can see, aboutListTileTitle is obtained by first retrieving MaterialLocalizations from the current build context.

Fixing the problem

Suppose you use intl_utils to manage ARB files in your Flutter application. The first step to solving the problem is to create a custom localization delegate class. This class should use MultipleLocalizations.load from the multiple_localization plugin to bypass the addLocale check:

class _LocalizationsDelegate<T> extends LocalizationsDelegate<T> {
_LocalizationsDelegate({
required this.isLocaleSupported,
required this.initializeMessages,
required this.builder,
});

bool Function(Locale locale) isLocaleSupported;
Future<bool> Function(String) initializeMessages;
FutureOr<T> Function(String locale) builder;

@override
bool isSupported(Locale locale) => isLocaleSupported(locale);

@override
Future<T> load(Locale locale) {
return MultipleLocalizations.load(
initializeMessages,
locale,
builder,
setDefaultLocale: true,
);
}

@override
bool shouldReload(LocalizationsDelegate<T> old) => false;
}

Then, you should construct your localization delegates in the following manner:

LocalizationsDelegate<app.S> _appLocalizationsDelegate = _LocalizationsDelegate(
isLocaleSupported: app.S.delegate.isSupported,
initializeMessages: app.initializeMessages,
builder: (locale) async {
await app.S.load(Locale(locale));
return app.S();
},
);

LocalizationsDelegate<ui.S> _uiLocalizationsDelegate = _LocalizationsDelegate(
isLocaleSupported: ui.S.delegate.isSupported,
initializeMessages: ui.initializeMessages,
builder: (locale) async {
await ui.S.load(Locale(locale));
return ui.S();
},
);

Lastly, simply use your delegates when calling the app’s constructor:

// main.dart

@override
build(BuildContext context){
return MaterialApp.router(
//...
localizationsDelegates: [
_appLocalizationsDelegate,
_uiLocalizationsDelegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
);
}

Please note that in the following example, all delegates should support the same set of locales. You can find the code for this example on our GitHub page.

Final thoughts

With sufficient effort, a lot is possible. Nevertheless, it would be great if a fix could be deployed for everyone as part of the intl_utils library.

We would also like to extend our gratitude to the team at Innim IT company for providing multiple_localization. Keep up the good work, guys!

--

--