Tuesday, June 22, 2021

Summer Flutter Development: Internationalizing the Thunderstone Quest Randomizer

Last week, I started laying down some of my Summer projects and began putting together course plans for Fall. I expected that, on Monday, I would jump in with both feet and get those new course sites up and ready. However, on Sunday, an unexpected thing happened on the GitHub page for my Thunderstone Quest Randomizer: a stranger posted a feature request to localize the app to French. 

I first wrote about this app about a year ago, when I described the reason for implementing a new randomizer in Flutter. Since then, I've released the odd improvement, including a significant internal improvement at the beginning of summer to allow the app to be built using null safety. That effort reignited my interest in the game, and so we've been playing a lot of it since then.

Sunday's request for localization support intrigued me for a few reasons. First, it showed that someone besides me actually looked at the open source code and cared that it was hosted up on GitHub. Second, the person who made the request is involved in proof-reading the actual French translation of the game, and it's neat to have "insiders" taking a look at a hobby project. Third, I talk about internationalization a bit in my classes, but I have never actually tried to internationalize a nontrivial application before. A quick look at Flutter's internationalization documentation intrigued me enough to make me want to get my hands dirty.

There were two major problems to solve regarding internationalizing the app: the app interface and the card database. It was pretty clear from early in my analysis that I could not easily use the same approach for both. I will address each in turn.

Internationalizing the app interface was mostly a matter of following the Flutter documentation. I brought in the Intl package and did the requisite configuration. I extracted all the user-facing strings into an application resource bundle file, which is a format I had not seen before. Authoring this JSON file involved a lot of tedious typing, and though I was on the verge of looking for tool support, I ended up just powering through it. I wrote descriptions for each string key, and I documented the parameters for each template string (which they call "placeholder resources"). Something that was not obvious to me from reading the documentation was that, while the intl tool converts regular string resources into dart literals, it converts placeholder resources into functions. This is quite convenient but it was not what I expected.

I used Visual Studio Code for all of my development, and it's worth noting that there's a slight hiccup in the toolchain when building internationalized applications this way. The arb file is transformed into dart code during the build process, but that build process is not triggered by saving the arb file. Writing UI code that uses the generated dart files then will give false-negative compiler errors until the whole application is reloaded. Fortunately, this can be done with the hot-reload tool. It's a workflow I got used to, but it seems like it would be convenient to hook the arb file's saving into a regeneration of the dart code.

Adding localization support to the card database was a more interesting technical challenge. The cards are all specified in a large YAML file that is loaded during application initialization. My original plan was to load only the localized names into memory, which would work fine if one only ever wanted the cards in their platform's default locale. However, when I got into it, I realized that I needed an easier way to change languages for testing. I had assumed there would be a toggle in Chrome that would simply change locale, but all the documentation I found said that it requires restarting the browser as well. That's too fiddly. I also ran into a synchonrization problem: the rest of the application assumed that the YAML would be parsed only once, but if changing the language required re-parsing, then the app architecture needed adjustment to account for the time this takes. 

After having written an almost complete solution using this approach, I abandoned it in favor of a  simpler one. Now, the YAML file is parsed only once, but all the localized strings are stored in memory in simple string maps. This certainly uses more memory than the alternative approach, but it is much easier to swap languages: using Flutter's simple state management tools of Providers and Consumers, the UI can watch for the language key to change and then alter which localized text is shown on the screen. I have not done any profiling to see what the actual memory costs are, and so I'm just banking on contemporary environments having memory to spare.

As of this writing, the app does not have French language support exposed because I only have the two resources localized that the original GitHub feature request gave as examples. However, it is set up so that uncommenting a single line in the settings view will expose language selection options. When someone wishes to localize the data into any language, then, it should be an easy matter to incorporate it into the app.

It was fun to spend a few days learning about Flutter's internationalization support and adding that to my app, even if right now, there's no difference to the user. I learned a few new tricks, and importantly, I gained some experience that I will be able to share with my students in the future. No more hand-waving about internationalization: I can show them an example of exactly how I've done it.

No comments:

Post a Comment