Thursday, October 13, 2016

Localize Plurals in iOS

Apple emphasizes internationalization and localization since its start of iPhone development. This is actually a very wide and difficult topic in some areas. Plurals in various languages make it really hard to code. In iOS, a solution is provided to get localized plural string and it is very nice and handy.

You can find the solution in many articles if you google it. Basically, the solution is based on a Plist resource file and couple of functions.

By default, the resource file is named as Localizable.stringsdict. Another file should be created as well: Localizable.strings along side even empty.

In the current Xcode 8, there is no stringsdict file type when you add from New File... What you can do is to use Plist file type, and then remove plist extension from the file name. In this way, the XML file can be viewed in ASCII Property List or a hierarchical format.

Plist File Structure

The property list is based on dictionary or dict node to define hierarchical structure. Each dict contains a list of key-value property. Each value can be a string or dict (children set of key-value list). The reason to use dict is that each key within a dictionary has to be unique.

This resource file contains three levels of dict. The top dict contains a list of resource keys. Each key can be used by function NSLocalizedString to retrieve a format string for the resource file.

  1. <plist version="1.0">
  2.    <dict>
  3.        <key>number_of_files</key>
  4.        <dict>
  5.            ...
  6.        </dict>
  7.        <key>working hours</key>
  8.        <dict>
  9.            ...
  10.        </dict>
  11.        <key># of entities</key>
  12.        <dict>
  13.            ...
  14.        </dict>
  15.    </dict>
  16. </plist>

The second dict level contains a list of key-value pairs. The first pair is used to a format for a plural string: by key NSStringLocalizedFormatKey, and its value as the format string. For example:

  1.           <key>NSStringLocalizedFormatKey</key>
  2.           <string>Total: %#@files@, %#@subs@</string>
The string value normally contains one or more variables. Each variable is marked by %#@ as preceding, and @ as ending. The variable should contain a numeric value, therefore, the result of the variable would be a localized plural string.

The remaining key-value pairs define plural rule for each variable. Each variable is a key, and the paired value defines plural rule by the third level of dict. For example, the following is English plural rule for the variable files.

  1.           <key>files</key>
  2.           <dict>
  3.               <key>NSStringFormatSpecTypeKey</key>
  4.               <string>NSStringPluralRulType</string>
  5.               <key>NSStringFormatValueTypeKey</key>
  6.               <string>d</string>
  7.               <key>zero<key>
  8.               <string>%d file</string>
  9.               <key>one</key>
  10.               <string>%d file</string>
  11.               <key>other</key>
  12.               <string>%d files</string>
  13.           </dict>
  14.           <key>subs</key>
  15.           <dict>
  16.               ...
  17.           </dict>

The first two key-value pairs in the third dict level are as-they-are standard, no change. The remaining key-value pairs are for language specific plural rules. For English, the plural rule has three categories: zero, one and other.

Refer to the resources for language plural categories.

Functions for Plural Strings

With above Plist file defined, the localized plural strings can be retrieved by following codes:

  1. let numFiles = 3
  2. let numSubs = 1
  3. let format = NSLocalizedString(" ", comment: "")
  4. let text = String.localizedStringWithFormat(format, numFiles, numSubs)

The first function NSLocalizedString gets a localized format string from Plist, and the second function localizedStringWithFormat retrieves corresponding plural string based on numeric values.

I have written a simple function to get localized plural string by extension to String.

  1. extension NSString {
  2.  static func localizedStringForPlurals(formatKey key: String, comment: String = "", args: CVarArg... ) -> String
  3.    {
  4.        let format = NSLocalizedString(key, comment: comment)
  5.        let result = withVaList(args){
  6.            (NSString(format: format, locale: NSLocale.current, arguments: $0) as String)
  7.        }
  8.        return result
  9.    }
  10. }

It is much simple to use the extension function in my project:

  1. let text = String.localizedStringForPlurals(formatKey: "number_of_files", comment: "Total: # of file, # of sub", args: numFiles, numSubs)
  2. // or
  3. let text1 = String.localizedStringForPlurals(formatKey: "number_of_files", args: numFiles, numSubs)