From the blog

Localized Pluralization with Stringsdict

It’s harder to create user-facing strings that contain numbers than it should be. This post should make it easier, with tips on localization and pluralization.

This post is not about all the good reasons to do localization (l10n) and internationalization (i18n). For that, I recommend the following excellent posts:


In English, there are only two cases, “one” and “other.” Examples:

  • “1 bug” vs. “10 bugs
  • “1 item is selected” vs. “2 items are selected”
  • That fish belongs to John” vs. “Those fish belong to John”

The right way to produce the above in iOS and macOS is:

  • use NSLocalizedString(_ key:comment:) to retrieve the format from a .stringsdict file
  • use String(format:locale:arguments:) to create the string to display.
1. Create empty .stringsdict

This is the most daunting part. Even though a .stringsdict file is a close relative of a .strings file, you won’t find a template in Xcode’s “new file” window.

To create the .stringsdict file from scratch, create a new empty file called “foo” in your resources folder, and insert the following:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">
<plist version="1.0">


Then rename “foo” to “Localizable.stringsdict.” It’s important to put the above boilerplate in before renaming, otherwise some versions of Xcode (including the flavor of 8.x that I’m using today) will not let you view the file and instead will tell you, “The data couldn’t be read because it isn’t in the correct format.” If you messed up, no worries, just rename the file so it doesn’t end in .stringsdict and you’ll be allowed to move on.

Note: if you close and reopen the file, Xcode will show it to you in a Plist-style editor. That can be useful, but purposes of copying and pasting, viewing the file as raw XML makes more sense. If you’re seeing the Plist editor, right-click on the file in Xcode’s project navigator (left pane) and choose Open As → Source Code.

2. Add your first item to the .stringsdict

Into that blank line, between <dict> and </dict>, insert this snippet:

    <string>%#@[email protected]</string>
        <string><#Optional string for zero#></string>
        <string><#String with %d for one#></string>
        <string><#String with %d for other#></string>

There are four placeholders in the above code snippet:

  1. Key is the same string you’ll pass to NSLocalizedString(_ key:comment:). Step 4 assumes you entered “n.items.”
  2. Optional string for zero lets you display “no items” instead of “0 items”. You can delete this entire line and the line above it if you don’t need a special case for zero items.
  3. String with %d for one is a string that you want to use to format one item. It must include a %d placeholder, which will be replaced with the number that you pass in. You can use any of the standard string format specifiers like %f if you need to, but this example will assume that you are using integers.
  4. String with %d for other is the version that will be used for all cases except “one” (and “zero”, if you specify one).

Note: the placeholders use the <#placeholder text#> format, which Xcode interprets as blue placeholder regions that you can move through with the Tab key. The use of < and > in the placeholder syntax also seems to throw off Xcode’s automatic indentation of XML code, but after you’re filled in the placeholders, you can select the code and use Control-I to auto-indent the file.

3. Write a utility function

The following function gets the entry from the .stringsdict and then uses it to format the string you’ll display to the user. (I got this particular implementation from SwiftGen, which we use to generate autocomplete-friendly enums for all our localized strings.)

func tr(_ key: String, _ args: CVarArg...) -> String {
    let format = NSLocalizedString(key, comment: "")
    return String(format: format, locale: Locale.current, arguments: args)

It may be helpful to note that the variable “format” isn’t a plain NSString or String, even though the compiler will tell you it’s a String. When the key is found in a .stringsdict file, NSLocalizedString returns an instance of __NSLocalizedString. If you look at in the debugger, you’ll only see a string containing the value you gave for NSStringLocalizedFormatKey, in this case %#@[email protected]. But all the cases you (or your localizer) put in the .stringsdict are there, waiting to act on whatever number gets passed to String(format:locale:arguments:).

4. Use the utility function

You’ll call it like this.

let title = tr("n.items", 2)

Fussy side notes:

  1. In production code, the first parameter wouldn’t be a string-constant. You’d use something that afforded you type-checking, such as the enumeration output by SwiftGen.)
  2. The snippet in step 2 uses %d as its format type, so you’ll have to pass an integer to tr(_:_), or change the %d.
  3. If you wanted to express multiple numbers in the same sentence, like “you have 2 goats and 3 sheep,” you can:
    <string>you have %#@[email protected] and %#@[email protected]</string>
        <string>no goats</string>
        <string>%d goat</string>
        <string>%d goats</string>
        ... as with goats, but with sheep


If your design uses ordinals (1st, 2nd, 3rd, 4th, etc.), you use a different solution. On iOS 9+, macOS 10.11+, NumberFormatter.Style has an ordinal case.

The code is very straightforward!

import Foundation

let fmtr = NumberFormatter()
fmtr.numberStyle = .ordinal

let s1 = fmtr.string(from: 1)
let s2 = fmtr.string(from: 2)
let s3 = fmtr.string(from: 3)
let s4 = fmtr.string(from: 4)

The variables s1…s4 get “1st”, “2nd”, “3rd”, “4th”. And best of all, this Just Works™ in other languages if your locale changes.

Sample Project

Download a Swift Playground illustrating the above.

iOS code for ordinals


Apple’s introduction of .stringsdict files

Stringsdict File Format

Common Locale Data Repository (CLDR) Plural Rules

CLDR Plural Rules Table – mind blowing, whatever your first language

Leave a Reply

Your email address will not be published. Required fields are marked *