Adding support for Dutch in my SwiftUI app was way more challenging than I expected.

My app, Museum Shuffle, retrieves random artwork from the Rijksmuseum in the Netherlands. Since Dutch is spoken there and many of the results from the museum are returned in Dutch I’ve always had supporting another language on my list of things I needed to do. I had no idea I would encounter so many complications when I attempted adding support for Dutch. Many of the reasons why were self-inflicted wounds that I’ll describe in detail.

I’m going to describe how I got out of some of the messes I created for myself. If you have comments on different ways I could have approached this I am all ears. I’m always trying to improve and learn how to do things better.

Background on my app

Museum Shuffle was actually the final project for an online course I took for developing apps with UIKit. Through several factors out of my control, including a death in the family, I found myself in a situation where I wasn’t sure if I was going to be able to finish the app by the course deadline. As I was scrambling writing the app as fast as I could the absolute last thing I had on my mind was supporting another language. Still, I did think that it would be better if all of my text that is presented to the user was put into the same place. So without doing any research (oof) on iOS Internationalization I came up with this system.

I created a struct named “Constants” that had multiple structs containing similar types of messages. My thinking was also that if I needed to use some of these more than once there was no chance I could make a typo.

struct Constants {
    struct Messages {
        static let APILong = "For years the Rijksmuseum has offered an API that lets developers access their artwork online. They encourage developers to use it in creative ways and ask for nothing in return. I thank them for their generosity."
        static let About = "About"
        static let Columns = "Columns"
        static let Delete = "Delete"
    }
       
    struct EmailMessages {
        static let Hello = "Hello!"
        static let Email = "Email"
        static let EmailImg = "envelope"
        static let DevInfo = "Developer Info"
        static let ReachFooter = "Please reach out if you have any comments/questions about the app."
        static let Subject = "Museum Shuffle Feedback"
    }
}

So to use one of these in a SwiftUI Text View, for example, it would look like this.

Text(Constants.EmailMessages.DevInfo)

Seemed good at the time but this would come back to haunt me later. 👻 I’ve continued to improve the app over the years and as you can imagine this collection of strings grew larger and larger.

Problem #1 - Strings file visibility

As I was following along with the steps Paul Hudson listed in this Internationalization and localization lesson I quickly hit a snag with what seemed like a simple step. He wanted us to create a localization file called Localizable.strings. The localization file uses a simple format.

"Home" = "Home";

However, he wanted us to change this so that the right side was different.

"Home" = "Fish";

With that change all instances of “Home” are replaced with “Fish” when you run the app. Except for me they weren’t. This confused me and was a pretty frustrating introduction to Internationalization. I was already stuck on part one! I finally thought to look at the file with Xcode’s file inspector. These two checkboxes were not selected.

My localization file with the appropriate items checked in File Inspector.

After this change the strings from my app I tried to add to the localization file still weren’t being translated. Luckily I had added a Text(“Home”) to my project as a test and that was being translated. What on Earth was going on?

Problem #2 - How I had stored strings

I didn’t realize it yet, but my design decisions for handling strings were coming back to haunt me. Paul’s next suggestion was to run this command: genstrings -SwiftUI *.swift. I ended up researching a variation that would go through subdirectories: find . -name “*.swift” -print0 | xargs -0 genstrings -SwiftUI. This command goes through all of your SwiftUI Text Views, extracts the text, and puts them into your Localizable.strings file. Except in my case it only did it for a tiny number of lines. That’s when it hit me. The type properties that I had set up to store my strings were killing me. I started experimenting and found that if I changed Text(Constants.EmailMessages.DevInfo) to Text(LocalizedStringKey(Constants.EmailMessages.DevInfo)) the translated version of my string would be displayed. That worked great but initially I wasn’t sure how I would insert LocalizedStringKey into well over 200 lines of different text. So that initially had me considering one of these options:

  • Manually replace every instance of a type property used in a Text View with the correct string it represents. With well over 200 strings this would be so painful and potentially error prone.
  • Do a search for “Text(” and replace that with “Text(LocalizedStringKey(”. That would work in most cases but it wouldn’t cover the closing parenthesis that would be needed. It would still leave me with over 200 lines where the number of parenthesis now don’t match and would cause a compile error.

What I really needed was to be able to use a regular expression and this blog post confirmed to me that was possible!

Here’s how I ended up updating all of my Text Views in Xcode. Instead of replacing text I replaced a regular expression.

A find/replace in Xcode using regular expressions.

This looks confusing but I’ll break down how it works. We’re searching for “all the text inside a Text View”. We have to use a backslash to escape every parenthesis that the Text View is using. The (.*) is the “all the text inside a Text View” part. We are replacing that with “a LocalizedStringKey inside of a Text View with all the text that was inside a Text View”. The $1 is how we access the “all the text that was inside a Text View” part. If we would have had an additional (.*) then we would access that second part with $2. Also notice that we’ve added an additional closing parenthesis because LocalizedStringKey added another opening parenthesis.

This fixed 99% of my Text Views. It didn’t work for my Alerts that used more than one Text View on the same line. Since I use git it was easy to have Xcode discard the change for a particular line and fix it manually.

I also did something similar for fixing other Views, such as Labels.

If there was a better way I could have handled this problem I’d be interested in learning it. This is just what I came up with on my own.

Problem #3 - Sending requests in Dutch

The whole key to my app working is the Rijksmuseum API. It’s a wonderful, absolutely free service that the Rijksmuseum has provided for many years for enthusiast developers such as myself that gives us access to information about a large part of the Rijksmuseum collection. When my app sends a request via the Rijksmuseum API it’s randomly selecting from a large number of object types (painting, bust, furniture, etc), materials (brass, porcelain, glass, etc), and techniques (glassblowing, gilding, etc). It then puts the selection in the request. I also let the user select these manually. Up until now I had hard-coded my requests to always ask for results in English. The Rijksmuseum API supports both English and Dutch. It’s been a few years since I looked at this logic. I assumed I would be able to “ask” for the things the same way and request that the results be in Dutch. That turns out to not be the case. In order to receive results in Dutch you have to ask for things in Dutch as well. In a regular scenario for translating an app the developer would just send the English localization file to a Dutch translator and be hands off at that point. But in my case the API is expecting requests in a very specific, exact wording in Dutch. In my last post I described how I transformed these arrays and their translated counterparts into a format expected for localization. Since I was going to have to ask for things in Dutch I realized that I would going to need to go through every one of the Dutch search possibilities to make sure that each one worked as expected. I was able to save time by doing doing searches for each Dutch search term via the Rijksmuseum website.

A lot of the items were straight forward but “vitrification” threw me for a loop. I didn’t even know what the English version meant! iOS Translate doesn’t support Dutch yet but hopefully that’s coming soon. For the translation of “vitrification” Google Translate returned “verglazing”, which was not a term the API accepted. I realized that the Rijksmuseum website would once again be key because it lets you switch between English and Dutch. I just had to search in English for a piece of art that was categorized under vitrification and then switch the website to Dutch.

"vitrification" = "glazuren";

Problem #4 - Missing semicolons

This is another problem where I was my own worst enemy. After going through the extensive list of Dutch search terms I realized that my app wasn’t compiling any more. When I would find that one of my translated Dutch search terms didn’t match what the museum expected I manually replaced it. Being a Vim enthusiast I did that by entering the command to delete from the cursor to the end of the line while then going into insert mode. The problem is that on a handful of those lines I forgot to put a semicolon at the end of the line. Xcode doesn’t tell you where the problem is in a strings file. It just displays a generic message at the top of the file. I couldn’t just do a ‘diff’ on the changes I had made since the last git commit because the diff output included adding all the Dutch search terms and modifying some of the search terms. A diff would just show one big chunk of text being added. Searching for them visually wasn’t working. Luckily one grep command is all that was needed!

grep -n -v ;$ file

This will look for the lines that don’t (-v) end ($) with a semicolon (which we need to escape with a backslash) while reporting their line number (-n).

October 21, 2021 Update

I got a great tip from Michael Hulet!

Sure enough the output tells you what lines have a semicolon problem.

plutil will tell you where the missing semicolons are.

October 21, 2021 Update #2

Another great tip from Swapnanil Dhol.

I’ve never opened a file as an ASCII Property List before!

Xcode submenu to open a file as an ASCII Property List.

Now Xcode will tell you what line number is missing a semicolon.

Xcode telling what line is missing a semicolon.

Problem #5 - Images not being saved

I fed the rest of my strings to Google Translate to get Dutch counterparts. I would never consider this the “final” version of the Dutch file, but it was good enough for testing. Now that I had a Dutch strings file to work with I encountered the most upsetting problem of them all. When I set the language in my Xcode simulator to Dutch I was horrified to realize that when I stopped and restarted the app some (but not all) of the images I had saved recently while the system language was set to Dutch had disappeared. The text associated with them was still present but the image was gone. This made absolutely no sense. All that I had thought that I had changed was text that is displayed to the user. Did I break this when I used regular expressions to change a large number of lines? Did I break this when I updated the project info to support another language? I don’t use translated strings to determine where I store things so that wasn’t it either. I decided to step through my app’s logic piece by piece to try and solve the mystery.

I never paid attention to how big the image files were that the museum sent back initially. But when I was adding support for my widget I realized that while usually the museum would send back normal-sized images sometimes it would send back enormous image files (28+ MB). I added some seatbelts to check for that and not use those monster images. To do this I was using ByteCountFormatter() to convert the image’s byte count to MB and then passing the result to Double().

        let bcf = ByteCountFormatter()
        bcf.allowedUnits = [.useMB]
        bcf.countStyle = .file
        bcf.includesUnit = false
        let mbSize = bcf.string(fromByteCount: Int64(bytes))

So if an image file was half a MB I would get back 0.5. Well when my system language is set to Dutch instead of getting back 0.5 I get back 0,5. That passed to Double() returns 0. So my code was in an unexpected state where it got past the seatbelt where I make sure the image data isn’t empty, but when I asked how much data there was I got back 0 bytes.

Time to translate!

Once I felt comfortable that the app was functioning normally all that was left was to send my attempt at a Dutch strings file to a translator. Big thanks to the world’s fastest app developer, Jordi Bruin, for making many corrections in Dutch.

Conclusion

I don’t want to give the impression that I just breezed through these problems. Some of them were incredibly frustrating and I’d be lying if I said I wasn’t tempted to quit at one point. I’m so glad I didn’t though because even though it was painful I learned so much from the experience. Now I can say that my app supports another language! The Rijksmuseum is the national museum of the Netherlands and it’s my hope that adding Dutch support will make the app more useful to many of its visitors.

If this post was helpful to you I’d love to hear about it! Find me on Twitter.