Introducing Twine- String Management for iOS, Mac OS X, and Android Development
I recently discovered that about 50% of our total app downloads at Mobiata originate from countries other than the United States. This really demonstrates the importance of properly supporting your international customers as such a large percentage of your sales depend upon them. However, localizing and translating your apps can be a very involved process.
In this post I hope to show you just how bad the standard localization process is for iOS and Mac OS X apps, and how we have found a way to make it much easier for developers to localize their apps and then maintain these localizations and translations over time. In addition, I’ll show you how you can easily share your translations across multiple apps and platforms. This will save your company money that would otherwise be spent duplicating your translation efforts, especially if you are developing for both iOS and Android.
The Standard iOS and Mac OS X Localization Process
The standard localization and translation process for strings in iOS and Mac OS X apps can be whittled down to the following key steps:
Go through your source code and replace all strings that need to be translated with appropriate calls to the
NSLocalizedStringmacro. For example, you would replace
@"Hello World!"with something like
NSLocalizedString(@"Hello World!", @"A comment for the translator about this particular string.").
genstringscommand line tool to extract all of the strings from your source code into one or more
ibtoolcommand line tool to extract all of the strings found in your various XIB files.
Send the files created in steps (2) and (3) to each of your translators. They will edit these files, replacing English strings with their appropriate translations. When they are done, the translators will send these files back to you.
.stringsfiles from step (2) should be included directly in your project. The translated
.stringsfiles from step (3) should be used by
ibtoolto generate a bunch of additional XIB files which should also be included in your project.
Manually go though all of your newly generated XIB files to make sure that your interface still looks good, especially in languages that tend to be a more verbose than English. Make adjustments to the layout of elements in these files as needed.
Yes, this is the standard process that Apple recommends for localizing and translating your apps. I, however, would not recommend this process to anyone, as I believe it to be fundamentally broken for a number of reasons.
The Standard Localization Process is Broken
genstrings does not do merging
genstrings is a very basic tool. It simply reads your source files and generates one or more
.strings files. It does not account for any translated strings that you may already have. For example, say that you released version 1.0 of your app with support for 11 languages including English. This means that you have 10 translated
.strings files that you shipped with your app. Now, let’s say that you want to release 1.1 which adds 12 additional strings that you need translated. What are you going to do?
You could run
genstrings again to create a completely new
.strings file for your translators, but there’s no easy way to communicate that they only need to pay attention to those 12 new strings. It’s possible one of your old strings changed slightly, too. Manually adding the 12 strings to your old, translated files is not especially productive. Some translation companies have systems that will help manage all of this for you, but many do not.
XIBs are not a good place for one-off modifications
After creating translated XIB files with
ibtool (step 5 above), I have often seen it recommended that developers edit each of those new XIB files to make any necessary modifications due to differences in strings lengths. These modifications can be anything from changing font sizes to completely resizing or rearranging some of the views. Doing this places too much knowledge of minute layout differences in your XIB files, which can make your applications hard to understand later. This brings us to the golden rule of working with XIBs: Never manually edit the files generated by ibtool. If you need to make modifications to the layout of your XIBs due to differences in string lengths, do so in code (
awakeFromNib is a great place for this). I think it is much easier to manage a single block of code that modifies layout than it is to manage 18 XIB files.
The English text is the string key
genstrings assumes that the English text for your string is also the string key, which is a horrible convention. What if you need to fix a misspelling in your 1.0.1 release? The translated copies of this string are most likely fine, but if you modify the English text (which resides in your source file), you are also modifying the string’s key. So, to fix a simple typo, you literally have to edit your source code to fix the string, and then update the string key in each of your translated
.strings files. If you support 12 languages this means editing 12 files to fix one typo!
There is another reason why using the English text as the string key is wrong. How do you handle words that mean completely different things in different situations? For example, take the word “list”. You could use it as a noun (the title above a
UITableView displaying a list of items) or as a verb (a button title describing the action of listing items on an auction site). While these may be the same word in English, they will most certainly not be the same words in other languages.
However, there is one important thing to realize about switching up your string keys: Apple did not implement a fall-back system for .strings files. This means that if a string key can not be found in your French
Localized.strings file, the system won’t use your English translation, even if it exists. Instead, your users will just see the string key. So, everything seems to work as you would expect if the English text and the string key are the same, but once you change it up and use an actual key for your string, you have to ensure that all of your
.strings files always contain all of your strings, whether or not they are actually translated into the appropriate language.
Your strings, comments, and translations are duplicated and scattered across many files
If you’ve used
genstrings before, you may have noticed how it handles finding the same string with two different comments. You are given the following nice warning message:
Warning: Key "Hello" used with multiple comments "First Comment" & "Second Comment"
If I use the same string multiple times in my project, I have to worry about whether or not I am using the same comment each time? I understand the importance of the warning message here, especially if the two comments are giving contradictory information, but as a programmer it’s not something I should have to worry about.
This brings us to an unfortunate fact about the “standard” way of localizing your apps: all of your string data is spread out across countless source code and
.strings files. Wouldn’t it be nice if they were all centralized in one location?
We decided that we needed a better way to manage our strings. We looked into a few applications and scripts that claimed to help with this painful process, but couldn’t find anything that we liked. Instead, we decided to build our own. Twine is an open-source command line tool written in Ruby that manages all of your strings, comments, and translations in a single text file. Twine parses this file and then generates all of the
.strings files for you. Or, if you’re building an Android app, Twine can generate Android
.xml string files.
Not only does Twine generate
.xml files, but it parses them, too. If your translation team is more comfortable editing Android files, just generate them and send them to your translator. When the translator is done, Twine can identify any strings that have changed and write them back to your master strings data file.
With Twine, we have complete control over the generation of these
.strings files. We don’t have to worry about our developers or translators forgetting to escape a double quote or forgetting to put a semicolon at the end of each line. And, if we have a change we need to make to a number of strings across all of our languages, we have a single, easy place to do so.
[yes] en = Yes da = Ja de = Ja es = Sí fr = Oui ja = はい ko = 예 [no] en = No da = Nej de = Nein fr = Non ja = いいえ ko = 아니오 [path_not_found_error] en = The file '%@' could not be found. comment = An error describing when a path on the filesystem could not be found. [network_unavailable_error] en = The network is currently unavailable. comment = An error describing when the device can not connect to the internet.
Twine also supports using only a subset of your strings when generating the output file. This way, you can have multiple apps across multiple platforms share the same strings data file. At Mobiata, we share the same data file across FlightTrack, FlightBoard, and Expedia Hotels. Sharing a string across apps and platforms is as easy as editing a single line in your master strings file.
We are very happy with Twine and hope that you like it and can find a use for it in your own projects. You can find more information about Twine (including a lengthy README) on its GitHub page. Or, you can install it right away as a Ruby gem.