Skip to content

Morphology in Swift

A guide to automatic numberless pluralization

by Michael Liberatore
A sketch of a single apple, labeled "Fig. 1: One" next to a sketch of a pile of apples labeled "Fig. 2: Many"

Morphology has been around for a few years now in Foundation, providing some seriously powerful conveniences for user-facing text, albeit with very little fanfare. With it, we can simplify logic around pluralization and gender as they relate to grammatical agreement, letting the system APIs do the heavy lifting. If you’ve heard of these APIs at all, you’re probably familiar with their headlining, and arguably magical, feature: inflection in interpolated strings.

Swift
// Produces the following in English depending on the value of `count`:
// - 0:  You have 0 new messages
// - 1:  You have 1 new message
// - 2+: You have 2 new messages
Text("You have ^[\(count) new message](inflect: true)")

If you’re not familiar with this, I encourage you to read through Jordan Morgan’s excellent post, Morphology in Swift, to wrap your head around this concise, yet powerful, concept.

That’s great, but my string doesn’t include a number 🤷‍♂️

You’ve come to the right place! We similarly struggled with wanting to use this feature without compromising what our designer had in mind for the UI. This is surprisingly difficult to search for, so congratulations on making it here.

When the user receives one or more messages, we want to let them know using natural language without displaying the count.

Depiction of an in-app notification when one or more new messages are received. The first screenshot shows the phrase “New Message,” implying that a single new message was received, while the second one shows the phrase “New Messages,” implying that more than one message was received.

So is this possible with inflection and string interpolation? Yes… Kinda.

After scouring the internet, picking up hints from forum discussions, finding bits and pieces in Apple’s documentation (like Morphology.Grammatical​Number), and leveraging the fact that Foundation is open source (see Morphology.swift), we found a working solution:

Swift
// Result: New Message ✅
Text("^[New Message](morphology: { number: \"one\" }, inflect: true)")

// Result: New Messages ✅
Text("^[New Message](morphology: { number: \"other\" }, inflect: true)")

What’s new here is the morphology structure. We haven’t been able to find sufficient documentation, but the aforementioned Swift forum discussion and some Apple-provided sample code put us on the right track. After all, if it’s possible to specify a part​Of​Speech in this morphology structure, why not a grammatical number?

Through trial and error, we attempted to figure out exactly what to specify here. Since part​Of​Speech works in the morphology structure, we’re reasonably sure we could use number, as both match their respective property names on Morphology itself. Grammatical​Number has six cases we can choose from:

Swift
// From https://github.com/apple/swift-corelibs-foundation/blob/018d8ef5497d030d0bce19b84764012c211d927d/Sources/Foundation/Morphology.swift#L45-L52
public enum GrammaticalNumber: Int, Hashable, Sendable {
	case singular = 1
	case zero
	case plural
	case pluralTwo
	case pluralFew
	case pluralMany
}

So naturally, we tried to plug these in, using their symbol names directly:

Swift
// Result: New Message ❌
Text("^[New Message](morphology: { number: \"plural\" }, inflect: true)")

But that didn’t work.

Back to digging. If we look at the implementation of Morphology.Grammatical​Number, we can see that it has a custom Codable implementation, specifying other names for each of these cases:

Swift
// From https://github.com/apple/swift-corelibs-foundation/blob/018d8ef5497d030d0bce19b84764012c211d927d/Sources/Foundation/Morphology.swift#L142-L168
extension Morphology.GrammaticalNumber: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        switch try container.decode(String.self) {
        case "one":   self = .singular
        case "zero":  self = .zero
        case "other": self = .plural
        case "two":   self = .pluralTwo
        case "few":   self = .pluralFew
        case "many":  self = .pluralMany
        default: throw CocoaError(.coderInvalidValue)
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .singular:   try container.encode("one")
        case .zero:       try container.encode("zero")
        case .plural:     try container.encode("other")
        case .pluralTwo:  try container.encode("two")
        case .pluralFew:  try container.encode("few")
        case .pluralMany: try container.encode("many")
        }
    }
}

Assuming that Codable is used to read and write morphology (see Markdown​Decodable​Attributed​String​Key), we tried again using the encoded representation of plural, which is "other":

Swift
// Result: New Messages ✅
Text("^[New Message](morphology: { number: \"other\" }, inflect: true)")

And that gives us exactly what we want!

Great, so how should I use this?

This is where things get a bit complicated and less convenient. Using interpolation to provide the value for number in the morphology structure is not supported. See the following example:

Swift
// Result: New Message ❌
let number = "other"
Text("^[New Message](morphology: { number: \"\(number)\" }, inflect: true)")

So to extrapolate, we can’t compute the value to use for number based on a count value that we know, e.g. let number = (count == 1) ? "one" : "other", and then use it with interpolation to produce the desired result. We could interpolate the actual text and use conditional logic based on our known count, which works:

Swift
let text = "New Message"
if count == 1 {
    // Result: New Message ✅
    Text("^[\(text)](morphology: { number: \"one\" }, inflect: true)")
} else {
    // Result: New Messages ✅
    Text("^[\(text)](morphology: { number: \"other\" }, inflect: true)")
}

But syntactically, that’s far less than ideal as it defeats the purpose of this being a concise approach — not to mention that we haven’t covered edge cases like -1 in English, which should read singular (irrelevant to our UI, but still), nor have we set ourselves up for success with differing pluralization rules in other languages.

Maybe if we cover all six cases (Morphology.Grammatical​Number), we can make some nice extensions out of this, starting with converting our known Int to a Morphology.Grammatical​Number, then switching over that instead of using if statements like we did above.

Swift
extension Int {
    var grammaticalNumberValue: Morphology.GrammaticalNumber {
        switch self {
        case 0: .zero
        case 1: .singular
        case 2: .pluralTwo
        case 3: .pluralFew
        case 4...Int.max: .pluralMany // Is this even right???
        default: .plural // I don’t know???
        }
    }
}

Hmm… we have to ask ourselves some tricky questions here. What’s the difference between plural and plural​Many? Is plural​Few actually 3? if you take a look at Apple’s docs, they don’t help to disambiguate:

case plural
Multiple persons or things, as used for a grammatical number.

case plural​Few
A small number of persons or things, as used for a grammatical number.

case plural​Many
A large number of persons or things, as used for a grammatical number.

“Multiple”, “A small number”, “A large number.” We’d need more precision than that to continue with implementing extensions. After all, we need to supply the right grammatical number associated with any possible Int. If you do a bit of searching around this phrasing (e.g. two, few, many, and other) you might stumble upon The Common Locale Data Repository, or CLDR for short. Lingohub provides a nice chart that lists languages along with the values/ranges that belong to each category:

Source: Lingohub, Pluralization (p11n) - the many of plurals. There are more languages than appear in this screenshot.

The categories exactly match the cases available on Morphology.Grammatical​Number, so if this is the standard Apple is using, our extension is inherently wrong. “Few” does not necessarily mean “3,” and other categories vary by language (see the official guidelines here). We can see that, suspiciously, English only has values specified for “One” and “Other.” Up to this point, we’ve only used one and other for our morphology number value. But surely, zero would work correctly, right?

Swift
// Result: New Message ❌
Text("^[New Message](morphology: { number: \"zero\" }, inflect: true)")

Wrong. In English, we’d expect the phrase to read “New Messages.” After all, you’d never say “You have 0 new message.” You’d say “You have 0 new messages.” So while perhaps the chart is right, our result is undesirable. But what about the first example in this post that actually uses the count in the string?

Swift
// Result: You have 0 new messages ✅
let count = 0
Text("You have ^[\(count) new message](inflect: true)")

It does work this way. It’s just sadly not the solution to our problem of trying to exclude the count from the string.

And just to be certain that we haven’t messed anything up syntactically (we are working with strings and stringly typed APIs after all), we can write a fully type-checked expansion of this:

Swift
let morphology: Morphology = {
    var morphology = Morphology()
    morphology.number = .zero
    return morphology
}()

let string: AttributedString = {
    var attributedString = AttributedString(localized: "New Message")
    attributedString.inflect = InflectionRule(morphology: morphology)
    return attributedString
}()

// Result: New Message ❌
Text(string.inflected())

But that doesn’t work either.

So rather than “how should I use this?”, perhaps the right question to ask is…

Should I even use this?

That depends on your specific conditions. Consider our working conditional example snippet from earlier:

Swift
let text = "New Message"
if count == 1 {
    // Result: New Message ✅
    Text("^[\(text)](morphology: { number: \"one\" }, inflect: true)")
} else {
    // Result: New Messages ✅
    Text("^[\(text)](morphology: { number: \"other\" }, inflect: true)")
}

We feel comfortable using this only if specific conditions are met and tested:

  1. We know the user will never have a negative number of messages.
  2. We know that the UI is not shown when there are zero new messages.
  3. We know that this app will only be used in English and possibly other languages that solely utilize the “One” and “Other” CLDR categories.

So it’s a fairly limited use case. But is it better than the following?

Swift
if count == 1 {
    Text("New Messages")
} else {
	Text("New Message")
}

Not really. The best we could do is extract this conditional logic of one vs. other to an extension, provided that all use cases of our new API meet the general conditions discussed above.

Swift
extension Text {
    init(_ text: String, countToInflect: Int) {
        if countToInflect == 1 {
            self.init("^[\(text)](morphology: { number: \"one\" }, inflect: true)")
        } else {
            self.init("^[\(text)](morphology: { number: \"other\" }, inflect: true)")
        }
    }
}

Then, future use cases can be simplified:

Swift
// Result: New Messages ✅
Text("New Message", countToInflect: 0)

// Result: New Message ✅
Text("New Message", countToInflect: 1)

// Result: New Messages ✅
Text("New Message", countToInflect: 2)

But again, we want to make sure this is only used in situations where our very limited conditions are met. Extracting it to an easy-to-use extension runs the risk of someone using the extension without fully understanding its limitations and implications. If you choose to go this route, be sure to provide clear documentation.

Other considerations and disclaimers

In conclusion…

While this specific use case isn’t something we’ll reach for in every project, it’s always fun to have a hunch that something should be possible, dive deep down into the rabbit hole, and come out learning a whole lot more than you bargained for. Morphology in Swift is a complicated topic that’s made fairly easy to use through nice APIs. In the coming years, we hope to see more language support and improved documentation to reduce the number of caveats to consider before leaning on this powerful solution.

TL;DR

Is it possible to achieve automatic pluralization in Swift without a number appearing in the string?

Yes

Is it concise to do so?

Fairly:

Swift
Text("^[<#string literal or interpolated string here#>](morphology: { number: \"<#`Codable` string representation of a `Morphology.GrammaticalNumber` here#>\" }, inflect: true)")

// For example…
// Result: Dogs
Text("^[Dog](morphology: { number: \"other\" }, inflect: true)") 
Does it work in every language?

No

Should I use it?

Only in carefully considered circumstances, and provided that the code savings are worth it to you.