By Will Braynen
Congratulations! You are bilingual. To celebrate your bilingualness, you want to make your app work in both English and Russian. Or maybe your employer has geoexpansion goals in mind for Q3. Yours is a healthy-eating app that helps users keep track of how many apples they’ve eaten today. Dynamically it has to display text like so: “0 apples”, “1 apple”, “2 apples”, “3 apples”. Or better yet: “No apples”, “One apple”, “2 apples”, “3 apples”, … And it has to do it in two different languages. There are two problems here to be sure: localization and pluralization. You could of course, instead of “N apples”, display “Apples: N” and then not have to worry about localizing plurals and pluralizing localized nouns. But “Apples: N” would sound choppy and look lame. So, in the name of better design and better UI, you decide to pluralize and go for “N apples”.
How hard can it be? Turns out, it’s a little tricky and rules for pluralization can vary from language to language. (NB: This article covers whole numbers and cardinals, not fractions or ordinals; that is, this article will not cover localizing “1st apple”, “2nd apple”, … , nor will it cover “1.5 apples”, albeit that’s not too far off.)
The problem
In English, there are only two forms: singular and plural. Apple and apples. But imagine a dialect of English in which there are different plural forms. For example:
1 applón
2 apploós
3 apploós
4 apploós
5 apples
If there are 2, 3, or 4 apples, then in this dialect that would be apploos. For example, one applon, but two apploos. In fact, because this dialect is of a poetic disposition and so likes rhyme, anything that ends with 1 is applon. For example: 21 applon, 31 applon, 1001 applon, and so on. Same with 2, 3, or 4. For example: 2 apploos, 22 apploos, 32 apploos, 1002 apploos, and so on. (Wait, 3 apploos does not rhyme! Well, 3 and 4 are in solidarity with 2, whereas 5 already thinks he’s big time.)
With the caveat that 12, 13, and 14, along with other numbers that end with 12, 13, and 14, behave like normal plurals—for example, it’s 112 apples and not 112 apploos (it’s an exception to an exception if you will)—this is in fact exactly the situation with Russian. Roughly the same is also true of Ukrainian, Polish, and Slovenian (except that Slovenian has an additional exception for 2, while 3 and 4 are in solidarity with one another). Czech is the same as the first three, but without the rhyme, while Bulgarian, while sharing an alphabet with Russian (which Russian actually got from Bulgarian), surprisingly, has only two forms just like English and German.
Don’t believe me? Go ahead and use your bilingualness to count apples out loud. Or, if you don’t have bilingual superpowers, type “1 apple” into Google Translate (or google “google translate”). As a native speaker, you probably never thought about it.
While the rules across languages differ, turns out the plural forms of all languages can be grouped into six buckets or, as Unicode calls them, categories:
zero
one
two
few
many
other
When, as an iOS developer, you add localization support to your app to handle pluralization (see below), magic happens. At runtime Apple, under the hood, calls IBM’s ICU C library (ICU stands for “International Components for Unicode”), which in turn utilizes the CLDR locale data to map N to the correct bucket above. N is the number of apples—or whatever it is we are counting. The mappings themselves can be found in the big Unicode table of which here is an annotated snippet:
(Yup, I filed a ticket with CLDR to correct that semi-typo in their documentation in the table above for Russian and Ukrainian and my ticket was accepted:)
The wrong way
I admit, I’ve written code like this myself:
if apples.count == 1 { let localizedText = NSLocalizedString("apple", comment: "Singular form of apple for text displayed on the Fruit-and-Vegetable Dashboard") label.text = "1 \(localizedText)" // "1 apple" } else { let localizedText = NSLocalizedString("apples", comment: "Plural form of apple for text displayed on the Fruit-and-Vegetable Dashboard") label.text = "\(apples.count) \(localizedText)" // "5 apples" or "0 apples" }
There are three things wrong with this code. The first is that apples.count
is not formatted using the current locale (e.g. “10000” is wrong even for the US; “10,000” in the US, but no comma and maybe a space in Europe). But more importantly, while this will work in English and even German, grammatically it will not generalize to many other languages. Writing plurals this way does not truly localize them because in some languages you will get ungrammatical text. Moreover, in languages that read from right to left (for example, Hebrew and Arabic), this will get the order wrong. (Go ahead, try it in Google Translate!)
The right way
At the very least, use a stringsdict file. (For example, see this this stackoverflow.) More on stringsdict in the next section.
When working with other people who will be doing your translations, best practice today, as of Xcode 9, is to use XLIFF files. Then, you can export to and import from your translator:
XLIFF is an XML format and, unlike Localizable.strings, is an industry standard rather than Apple’s own thing. The XLIFF workflow at which the above screenshot hints is worth understanding and using. To learn more about it, watch this WWDC 2017 talk.
And don’t forget to fill in the comment
field in your NSLocalizedString()
calls to provide some context to the translator. Otherwise, your translator will receive an XLIFF file that unhelpfully says this:
Stringsdict
As you will see in the WWDC talk, to support localized pluralizations using Xcode, you will have to add a “stringsdict” file (a strings dictionary) and you will have to call it “Localizable”. The Localizable.stringsdict
file is a plist. It’s where your pluralized translations can go (along with adaptive-width strings).
Tip: You do not need to localize Localizable.stringsdict if your app currently only supports English (or whatever the base language).
The format of the stringsdict plist is a little funky. Given two languages—English and Russian—you would have to worry about doing three things to your stringsdict file(s):
There are really two files with the same name but under different folders on disk, assuming you have localized this file in Xcode for both languages. So, first, in both the English and the Russian versions of your Localizable.stringsdict file, you should set the “Localized String Key”. Because it’s a key, it will have the same value in both files. For example: “%d apples” or “apples”—as long as you are consistent, probably doesn’t really matter which (aside from the translator seeing it in your XLIFF file if you are using that workflow).
Second, enter your English translations. In other words, in the English version of the stringsdict, you would want to enter the templated versions of the English text you wish to display to the user. (In English, translations for
one
andmany
forms are mandatory.)Third, do the same for Russian text in the Russian version of the stringsdict.
Here are illustrations for steps above:
You could in fact use a stringsdict to pluralize in English even if English is the only language your app supports. This possibility makes it clear that you can have pluralization without localization (if localization implies having more than one language or locale to worry about). And because a stringsdict can scale to other languages, whereas trying to pluralize in code with if-then statements would not be, this might be the more professional way to go anyway.
sample project
Sample Xcode project (Swift 4 | Xcode 10). This project illustrates the simple case of monolingual pluralization in a way that scales to handling more languages.
Slight hack
Given a normal workflow, you likely wouldn’t want to do this. So I do not mean to suggest this as a best practice. (Much depends on the actual workflow you already have in place or whether you are doing greenfield development.) But it is interesting. If you wanted to keep all your translations in your Localizable.strings file, including the pluralized templated translations in all their relevant forms (e.g. one, few, many, other), then you could do the following.
First, modify your stringsdict plist(s) so it always says the following, keeping the category rows that are relevant to the language:
Because this stringsdict will now serve for all of your languages, you do not need to localize it. One (unlocalized) copy is enough.
Second, add the following: (code snippet: scroll me to the right!)
func NSLocalizedStringPluralized( withLocBaseKey locBaseKey: String, withCount count: UInt) -> String { let stringsdictKey = "categoryEndingKey" // Key for Localizable.stringsdict file let categoryEnding = String(format: NSLocalizedString(stringsdictKey, comment: ""), count) let locKey = "\(locBaseKey)\(categoryEnding)" // Key for Localizable.strings file // Using String.localizedStringWithFormat to first run the number through NumberFormatter for current locale (e.g. "10,000" or "10 000"?) return String.localizedStringWithFormat(NSLocalizedString(locKey, comment: ""), count) }
NSLocalizedStringPluralized, under the hood, triggers a call to the ICU library to look up the appropriate categoryEnding
(e.g. “One” or “Many”) in the stringsdict file. Then it fetches the right entry from Localizable.strings.
Your call site:
let label.text = NSLocalizedStringPluralized( withLocBaseKey: "apples", withCount: apples.count )
And now you know! (You—singular, plural?)
References
Unicode’s Language Plural Rules, which above I dubbed as the “big Unicode table”.
ICU, which Apple ships it with iOS as a private API. That is, Apple uses ICU with CLDR under the hood. Also, ICU’s PluralFormat class.
ICU for Swift for the curious. Not what Apple uses, but interesting and provides some extra functionality. You could even use this yourself and ship it with your app if you statically link it (or else Apple will reject your submission).
Apple’s documentation: Stringsdict File Format and String Format Specifiers.
WWDC 2017 Session 401 talk and Apple’s documentation on Localizing Your App.
“Where the &$!#% is Localizable.strings?!?” by Eric Blair.
Ordinals: “Localized Pluralization with Stringsdict” by Eric Slosser, which mentions how to handle ordinals. However, though I haven’t tried his solution, I am a little skeptical that it will do all that it should outside of English. For example, nouns in Russian are gendered; and in some languages the noun might even have a different pluralized form from its cardinal counterpart for the same exact value of N, where N is the number of apples or what have you. Will Eric’s
NumberFormatter().numberStyle = .ordinal
solution handle all that? But like I said, I haven’t tried.