Lessons from internationalizing Trello, part I: plurals on iOS

On page 52 of my copy of K&R, in a discussion of the ?: operator, is this line of code:

printf("You have %d item%s.\n", n, n==1 ? "" : "s");

And thus began my decades-long proliferation of plural-unfriendly strings. I would later learn that not all languages base pluralization on whether the relevant number is one or not.

In my defense, my other projects were either stringless, too young to be internationalized, translated into languages with English-compatible pluralization rules, for internal usage, or ok with strings like "You have %d item(s)".

Also, Brian Kernighan tricked me.

Trello is now in 21 languages, but our first localized releases were to Spanish, German, French, and Portuguese, which all pluralize like English. Earlier this year we added support for Russian, Czech, Chinese, and a few others with different rules. To support those languages, we used the Unicode Common Locale Data Repository's pluralization rules for each language.

Their approach is to define six categories (zero, one, two, few, many, other) and then, for each language, describe a formula to pick a category based on the number in the sentence. Most languages don't need all of the categories. For example, in English, we use "one" for 1 and "other" for everything else.

So, as an American, I just need to know if a number is one or not in order to speak grammatically correct sentences, but in Russia, every five year-old can do this math in their head while speaking:

one → n mod 10 is 1 and n mod 100 is not 11
few → n mod 10 in 2..4 and n mod 100 not in 12..14
many → n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14
other → everything else

which is harder than FizzBuzz.

We use Smartling.i18n, an iOS Framework that implements these rules. It requires a suffix on your string key, so in Localizable.strings, for a string that needs plurals, you do something like this:

"member_count##{zero}" = "No members";
"member_count##{one}" = "One member";
"member_count##{two}" = "Two members";
"member_count##{few}" = "$COUNT$ members";
"member_count##{many}" = "$COUNT$ members";
"member_count##{other}" = "$COUNT$ members";

The $COUNT$ is because we use chrome.i18n string interpolation (I'll explain in a future installment).

English doesn't have a "zero" category according the CLDR, but I added one because otherwise 0 would fall into the "other" category, and we'd say "0 members" instead of "No members", which we think is un-Trellolike.

We provide all six strings for English so that there's a place-holder for translators. It also future-proofs us to changes in English.