Localizing Plurals in iOS Development

One of the biggest tells of software that hasn't been tested properly or rushed through development is when you see a label such as 1 days remaining (instead of 1 day remaining).

A quick fix for this is to check specifically for the number 1 and pluralise accordingly:

let message: String

if numDays == 1 {
    message = "1 day remaining"
}
else {
    message = "\(numDays) days remaining"
}   

Great! Problem solved!

Although, since we're internationalising our code it needs to be put into localizable.strings:

number_of_days.one = "1 day remaining";
number_of_days.other = "%d days remaining";

The above code now needs to be changed to use these translated strings:

let message: String

if numDays == 1 {
    message = NSLocalizedString("number_of_days.one", comment: "")
}
else {
    message = String(format: NSLocalizedString("number_of_days.other", comment: ""), numDays)
}   

Great! Problem solved!

Although in some Arabic-speaking countries, their numerals look a little different. Instead of 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, they use ٠‎ (0), ١ (1), ٢ (2), ٣ (3), ٤ (4), ٥ (5), ٦ (6), ٧ (7),‎ ٨ (8),‎ ٩ (9).‎

You can see this for yourself. Try creating a new Playground in Xcode to check the output of this code:

// Western Arabic (United Arabic Emirates)
String(format: "Number %d", locale: NSLocale(localeIdentifier: "ar_AE"), 1)

// Eastern Arabic (Saudi Arabia)
String(format: "Number %d", locale: NSLocale(localeIdentifier: "ar_AR"), 1)

Okay, that's easily fixed: Just don't hardcode numbers in your translations. Update your Localizable.strings file accordingly:

number_of_days.one = "%d day remaining";
number_of_days.other = "%d days remaining";

And now you need to update the output code so the number 1 is substituted in like it is for plurals. In the code above, we used String(format:), but this won't localise the numeral automatically. Instead, we'll now use String.localizedStringWithFormat(), which will automatically use the current locale to decide which type of numeral to output.

let message: String

if numDays == 1 {
    message = String.localizedStringWithFormat( NSLocalizedString("number_of_days.one", comment: ""), numDays)
}
else {
    message = String.localizedStringWithFormat( NSLocalizedString("number_of_days.other", comment: ""), numDays)
}   

Great! Problem solved!

Although in some languages, plurals don't work the same way as they do in English. Some languages don't indicate plurals (such as Japanese), while for others the word will change depending on the quantity (such as Russian).

To make things a bit easier, iOS has categorised the different plural types as follows:

  • Zero. Used to indicate a 0 quantity.
  • One. Used to indicate a quantity of exactly 1.
  • Two. Used to indicate a quantity of exactly 2.
  • Few. Used to indicate a small quantity greater than 2, but this depends on the language.
  • Many. Used to indicate a large number, but this also depends on the language.
  • Other. Used to indicate every number that isn't covered by the above categories.

Not all languages need all categories specified, since all work differently. At first glance, it would appear that English would require rules for zero, one and many. However, the plural rules for English would be specified with one and other, since all numbers apart from 1 are treated equally.

To specify the plural rules, you need to use a strings dictionary (Localizable.stringsdict) instead of a regular Localizable.strings file. In actual fact, you'll need the .strings file to still be present for the .stringsdict to work, even if it's empty.

In Xcode, create a new Plist file, and name it Localizable.stringsdict. Once populated, the raw data in the Plist will look as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>number_of_days</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@value@</string>
        <key>value</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>%d day remaining</string>
            <key>other</key>
            <string>%d days remaining</string>
        </dict>
    </dict>
</dict>
</plist>

This is a bit of a mouthful, but here's what each line means:

<key>number_of_days</key>
<dict> ... </dict>

This is the name of the string. You use this value in NSLocalizedString() to retrieve the plural information (shown near the bottom of this article). Its corresponding value is a dictionary containing all of the plural translations.

<key>NSStringLocalizedFormatKey</key>
<string>%#@value@</string>

This is the formatted string that will be output. The translation will be substituted into %#@value@. There can be multiple substitutions, but we're only doing one in this case (called value).

<key>value</key>
<dict> ... </dict>

This dictionary specifies the rules for the placeholder called value. If there were multiple values being substituted into the NSStringLocalizedFormatKey string, then there would be another dictionary accordingly for that placeholder.

<key>NSStringLocalizedFormatKey</key>
<string>NSStringPluralRuleType</string>

There are other uses for a .stringsdict file, but in this instance we're using it for plurals, so this entry specifies that fact.

<key>NSStringFormatValueTypeKey</key>
<string>d</string>

This entry indicates the type of value that will determine the plural rules. d refers to a decimal number.

<key>one</key>
<string>%d day remaining</string>
<key>other</key>
<string>%d days remaining</string>

These are the specific translations for the given categories (one and other). Depending on the language, you may have more categories (some languages you may only need the other category).

Finally, you can bring this pluralised translation into your code. Earlier we checked within the code to determine which translation to bring in, but now you can let iOS handle this for you:

let format = NSLocalizedString("number_of_days", comment: "")

let message = String.localizedStringWithFormat(format, numDays)

It takes a little bit of forethought and setup, but it makes internationalisation far simpler!

Great! Problem solved!


If you're looking for translators for your iOS or tvOS apps, try out ICanLocalize. We've used them for many apps and will continue to use them.