Flutter Localization & Translations
--
Have a Multilingual Flutter App in a Snap!
Followers of my articles will possibly remember the article, Do You Speak A-My Language?! It involved a simple text file (see below) as the source of the translations with columns of words separated by commas. Such a file is more commonly referred to as a CSV file. Each row would be the translation of the first word in that row with different translations in each column. It was a good idea for maybe two or three separate languages but frankly, it became a bit too unmanageable beyond that. One day, while working on a client’s app, I found a more practical way of handling translations. You see, this project happened to be using GetX.
If I’m correct, GetX originally began as an extensive Service Locator. However, as many of you know, it has since evolved to include as its documentation states, ‘state management, intelligent dependency injection, and route management.’ However, the one feature I was drawn to was how the author of GetX, Jonny Borges, handled text translation using an extension of Flutter’s String object.
The client’s app had these little ‘.tr’ suffixes on many of its String objects. ‘What the heck is that?!’, I said to myself. Tap on the screenshots below for a closer look. Turns out, tr, was a getter from the String extension, Trans. No doubt, ‘Trans’ stands for Translations. I liked this implementation. So much so, I stole it!
Yup, I took Jonny Borges’ idea of using the .tr suffix on String objects. However, I did reach out to him to let him know — the least I could do I’d like to think. I took his idea and ran with it. As a result, this article will introduce a newly released package called, l10n_translator.
I Like Screenshots. Tap Caption For Gists.
As always, I prefer using screenshots in my articles over gists to show concepts rather than just show code. I find them easier to work with frankly. However, you can click or tap on their captions to see the code in a gist or in Github. Tap or click on the screenshots themselves to zoom in on them for a closer look.
No Moving Pictures, No Social Media
There will be gif files in this article demonstrating aspects of the topic at hand. However, it’s said viewing such gif files is not possible when reading this article on platforms like Instagram, Facebook, etc. They may come out as static pictures or simply blank placeholder boxes. Please, be aware of this and maybe read this article on medium.com
Let’s begin.
Included with the package, l10n_translator, is an example app with the translation of a number of arbitrary English words to either Arabic, Spanish, French, Hindi, Korean, Portuguese, or Chinese. As you see in the gif file below the example app presents an English word in the first column and the translation if any in the second column. A screenshot of that code is also listed below. Note, Jonny’s String extension reappears in this code. I’d like to think the similarities between GetX ‘s implementation and that of l10n_translator’s ends there, but there’s another idea I ‘borrowed’ from Jonny that’ll I’ll bring up shortly. Until then, looking at that code, I do find it to be very — clean. Don’t you?
Let’s walk through this example app and see what’s involved in supplying these translations. We’ll go through the steps needed. First, as prescribed on the Flutter website, setting up the Internationalizing in Flutter apps involves a few things. For example, for your app to support more Localizations other than the one default, US English Localization, you are to include
the package, flutter_localizations, in your pubspec.yaml file. See below. Further, the app’s MaterialApp or CupertinoAopp widget would require their properties, locale, and supportedLocales, to be assigned the appropriate objects.
Again by default, the property, locale, is assigned the Locale, en_US. Of course, you would assign it another Locale object if you’re originally writing your app for another country. As for the property, supportedLocales, it is to take in a list of Locale objects indicating what other Localizations the app is to also support.
By the way, before we continue, the screenshot below is of all the Locale objects I would traditionally provide my apps. You see, I’m based in Canada and my clients were all working and living in English-speaking countries, so this would be sufficient in most cases. It was only when projects started going global, that text translation had to be considered, and I didn’t like Flutter’s approach. I found it cumbersome…and ugly.
In any case, I would suggest getting into the habit of supplying those parameters values every time — even if you’re in the United States. Now, back to this particular example app. In this case, the list of Locale objects passed on to the named parameter, supportedLocales, also indicates the text translations this app is capable of doing. See the second screenshot below.
It’s in the second screenshot above that we see the class, AppTranslations, being instantiated. Its property, textLocale, indicates the Locale represented by the non-translated String objects that make up the text for this app’s interface. Know that in this app, they all will likely be appended with the .tr extension — the same approach taken in the Get package.
UPDATE: Note, the rest of this article describes the package’s implementation. That implementation has evolved since this article and could be misleading. For example, the three classes described in this article have all merged into one since this article was published.
The first screenshot below is also of the example app. You can see that the property is assigned the Locale, en_US. Although in Flutter, en_US is the default, it’s still required by the abstract class, L10nTranslations, in which the class, AppTranslations, extends as a subclass. You can see the second screenshot below is of this abstract class, and indeed its getters are to be implemented. A third getter not seen below is called, supportedLocales, and you’ve already seen it passed to the MateriaApp Widget displayed above.
The getter, l10nMap, in the first screenshot above is now getting us to the interesting stuff. You can see it’s a Map that contains another Map as its value field and a Locale object as its key field. Interesting no? Looking closer, you can see the Locales listed above contain the ‘languages’ I had mentioned earlier: Arabic, Spanish, French, Hindi, Korean, Portuguese, or Chinese. By the way, the value fields you also see above storing the ‘String, String’ Map are memory variables. We’ll get to them shortly.
Do you see what’s going on up to this point? To allow for this package’s translation capability, you have to implement the abstract class, L10nTranslation, and explicitly lay out what Locale your App originally uses as well as the further Locales it supports and thus can translate. Easy peasy. By the way, the prefix, L10n, is in reference to the series of standards called on to sustain software’s adaptation, application, and support of cultural and regional differences around the globe. There is the term, i18n for Internationalization, L10n for Localization, and g11n for Globalization.
Anyway, let’s get back to it. Again, the abstract class also has the getter, supportedLocales, but it’s been implemented already and will prove to be a crucial piece in this whole process. As you see in the first screenshot below, this third getter takes in the implementation of the first two accessing what seems to be the static references from a class called, L10n. However, L10n is not a class. It’s a high-level variable as you see in the second screenshot below.
This is another idea I took from Jonny Borges and his Get package. You see, when you implement his Get package, he has you prefix all its functions and features with the word, Get. See the first screenshot below. Now that’s not the name of a class but simply a memory variable! It’s a ‘high-level’ memory variable defined openly in the Dart file, get_main.dart.
Admittedly, I would not have thought of it: defining a lone high-level variable as final that takes in a Class (serving as an Interface in this case) and then reference that class’ properties, functions, and such with that very variable. You can see the compiler complains of this a little bit with the warning: ‘non-constant identifier name’. It’s a variable not beginning in lowercase. Mr. Borges chooses to ignore this warning, and I got to say it’s an interesting approach. And so, I chose to ignore the warning as well and use this approach for my L10nLocale class. See the second screenshot below.
However, I’m getting a little ahead of myself here. We’ll talk more about the L10Locale class shortly. Let’s take a look back at those ‘Map’ memory variables first introduced in the class, AppTranslations. It’s where the getter, l10nMap, was being defined. Below is the screenshot of that getter again. You’ll discover they too are ‘high-level’ memory variables (variables in Flutter not defined within a class but merely in a Dart file with no leading underscore).
You can see in the second screenshot below they are all stored together in one directory. Each language is given its own dedicated Dart file with the key field being the non-translated word and the value field containing the appropriate translation. See how that works? Personally, I feel this makes for easier maintainability in the long run. With any additional translation, simply add a new Dart file to the directory. Yeah, better than a massive CSV file as was my previous approach.
So how does the actual translation play out, you ask. Well, I’ll show you. Let’s return to the l10n_translator package’s example app and step through the code involved. Let’s see if I can explain this without making this article too much longer a read. Below is the gif file again demonstrating the example app.
The best way to explain this is to start with the menu: What happens when you tap on a different ‘language’ in that popup menu? Below, in the first screenshot, you see the PopMenuItems being defined. You again see the class, AppTranslations, is used here. This time, it calls its function, setAppLocale(), when the user taps on a menu item. By the way, the class, AppLocales, is only specific to this example app and is merely used to supply ‘human-friendly’ names for each language available in the popup menu. You can see this in the second screenshot below.
So, it’s the setAppLocale() function that manually changes the App’s Locale. Without getting too lost in the logic, know that the function, setAppLocale(), assigns the provided Locale to now be the App’s Locale and not necessarily the device’s Locale. Heck! It may not even be the exact Locale the user is currently running the app! It could get a little complicated, but know that this function is only concerned with the Locale the running App is to work with at that moment, and so the App is to then behave accordingly. In this case, it means to then to provide the appropriate translations if any for that particular Locale.
In the second screenshot below, you see the String extension with its getter, tr, calling the function, translate(), from the class, L10nLocale. We’re coming to the finish line now: It’s there in the function translate() where either the appropriate translation is found and returned, or if not found, the very String object itself and its current value is returned instead. Tap the third screenshot.
That’s pretty much it. Every String object with the .tr extension will be calling that translate() function — lag in performance being negligible. However, we are missing one more important piece of this process. I mean, making your app capable of manually selecting a different language to then translate all its visible text will not be the primary use of this Dart package. On the contrary, that ability will likely not be used much at all. Its primary use is to allow an app to be distributed to an array of customers anywhere in the World and provide whatever service it’s intending to provide in that user's native language. Well, how does it do that?! Well, I’ll show you.
The first screenshot below is again that of the example app. It’s the build() function that returns that app’s interface. Notice the line highlighted by the little red arrow? The command, L10n.locleOf(context)
, is there to determine the phone’s (and likely the App’s) current Locale, and is it at that location so a change in locale can be readily detected.
Now, why would you want your app to detect a change in its locale? Why would you want your app to change languages (i.e. Localizations) when the locale changes? I have no idea. It’s your app, not mine! All I know is it’s an option that needs to be considered, and so below in the second screenshot, you can see passing the named parameter, allowLocaleChange, the boolean value, true, will allow for just that. Otherwise, it keeps the original locale it started with. You would likely preserve that locale in your App’s preferences, but that’s for another article altogether.
Now, I personally don’t like the idea of relying on a ‘context’ object to get an app’s Locale, and I did a Google search to find alternatives. However, Rémi Rousselet suggests it’s the best choice in the post, Localizations without calling `of(context)` every time. With him being the author of a little package called, Provider, I’ll take his word on that for now.
By coincidence, the original poster was commenting on how Flutter’s own approach for translating text was rather ‘ugly’ and asked, ‘Is there a way to make the usage nicer?’ Maybe we have the solution right here. What do you think?
Cheers.
TL;DR
Of course, in my own little effort to provide a working framework to the Flutter community, I’ve incorporated l10n_translator into my mvc_application Dart package. It too has an example app that demonstrates how text translations can be easily implemented. For example, a screenshot of the ‘Counter app’ code is displayed below with its ‘tr’ suffixes on the appropriate String objects. It’s one of three apps conveyed in my framework’s example app and all allow for text translation with a single tap. Take a look if you like. I dare you. :)
Some free publicity didn’t hurt anyone, and so here’s a free article further discussing my own contribution to the Flutter community.