The Apple App Store is a huge market with potential users and customers from all over the world. With this global reach, it’s more important than ever that your app is ready to be used by people that don’t speak English. For example, at PSPDFKit, a PDF framework that is used by a lot of apps in the App Store, we localize our framework in 29 languages, so that users don’t see parts of an app in English and parts in their local language, which would be a bad experience.
One of the challenges of localization lies in the length of translated texts. Languages like German are especially problematic because of its longer texts compared to English. In addition to that, translators are often working with isolated strings, where the only context they get (if they get any) is the place where the string is going to be placed and its purpose, but they don’t usually have any idea about the available physical space on the screen. Moreover, the available space may not be constant as the same app may be run on an iPhone or an iPad (or even a Mac in the future?). This generally leads to a not so ideal user experience: either the text is cut with ellipsis on some devices, or the same text has different sizes depending on the language, or we decide to arbitrarily shorten the localized text for a particular language, creating a lot of wasted space for iPad Pro users, and penalizing them with less explanatory strings.
In order to solve this problem, if you don’t want to engineer your own solution Apple introduced “adaptive strings” with iOS 9. This feature is based on string dictionaries (.stringsdict files), which are commonly used to support pluralization rules in apps. If your app does not already have a .stringsdict file, you can create one using Xcode: Go to File, New, File (or press Command+N) and select “Stringsdict file” from the template list.
Once the Stringsdict file is created, you have to modify its contents so that it supports adaptive strings. For each key that you want to support multiple localizations, add a
NSStringVariableWidthRuleType dictionary with key/value pairs, one for each “class” of screen width that you want to support. The key must be a number that represents in an abstract way the screen width (more on this later), and the value is the localized string. Here’s a sample .stringsdict file showing different possible welcome messages in Spanish:
You can download a sample Xcode project from here. If you compile and run the app on an iPhone 5s, 6, and iPad Pro you’ll get three different experiences: You’ll read the shortest localized string (“Hi”/”Hola”) on an iPhone 5s, “Welcome”/”Bienvenido” on an iPhone 6, and “Welcome to my app”/”Bienvenido a mi app” on an iPad Pro.
What do the 20, 25, 50 numbers mean?
They look like “magic” numbers, but they do actually have a meaning: they intend to abstract the width that is available to show text. Let’s see how UILabel presents localized text on the screen when variable width strings are in place:
When you call
NSLocalizedString("STRING_KEY", "String context"), this macro returns an NSString instance that you can set to a UILabel via its
text property. Very simple API. However, remember that NSString is actually a class cluster, that is, a group of classes that are exposed via a single public abstract superclass. This design pattern was used in NSLocalizedString when the support for plurals was added. With the introduction of adaptive strings, the same design pattern was used, and what the function actually returns is an internal subclass of NSString,
__NSVariableWidthString. When you set the text of a UILabel, the system internally queries if the NSString that you are passing is an instance of
__NSVariableWidthString and, in that case, extracts an appropriate text variant. There’s a public category on NSString inside NSBundle to do this: variantFittingPresentationWidth(_:) Its public documentation is empty, but the header file explains a little bit more:
If you feel that the public documentation of this method should at least contain the comments of the header file where it is declared, please duplicate this Radar. UILabel calls this API when you set the text, but how does it generate a value for the
width parameter? The header documentation explains that this heuristic is basically decided by the client. UILabel first extracts the width of the UIWindow where it is placed. Then, it creates an NSAttributedString instance with an uppercase “M” and a default font for body text. Dynamic type, introduced on iOS 7, helps with this by providing convenient API:
let font = UIFont.preferredFont(forTextStyle: .body) The
width parameter is calculated as the number of “M”s that can fit inside the UIWindow’s bounds width, that is,
The important thing that you need to remember about this heuristic is that it is directly proportional to the screen width and that the exact numbers you put in the .stringsdict file are not as important as the relationship between them. For example, you could also create two keys: 1, for small devices, and a bigger number like 30 to have a more detailed translation on devices with more available space. This is the approach that Apple follows for most of iOS stock apps.
To sum up, Apple provides support for adaptive localized strings via .stringsdict files in two ways:
- If your codebase uses UIKit components, you don’t need to do anything, the system will show appropriate adaptive strings if you add a .stringsdict file in the way described in this article.
- If you don’t use UIKit components or you need more flexibility, simply use the
variantFittingPresentationWidth(_:)instance method of NSString to manually get the text from the desired key in the .stringsdict file. Something like this:
variantFittingPresentationWidth(_:) also returns an instance of the internal type
__NSVariableWidthString, so if you set the result of this API to the
text property of UILabel, it will always be overridden by UIKit’s own heuristic. This has confused people in both Apple’s developer forums and StackOverflow. One possible solution to this is to use string interpolation:
welcomeLabel.text = "\(string.variantFittingPresentationWidth(25))"
And that’s it, I hope you enjoyed this article and maybe inspired you to build something on top of this API to improve your localization workflow.