Morphology in Swift
A guide to automatic numberless pluralization
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.
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.
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.GrammaticalNumber
), and leveraging the fact that Foundation
is open source (see Morphology.swift
), we found a working solution:
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 partOfSpeech
in this morphology
structure, why not a grammatical number
?
Through trial and error, we attempted to figure out exactly what to specify here. Since partOfSpeech
works in the morphology
structure, we’re reasonably sure we could use number
, as both match their respective property names on Morphology
itself. GrammaticalNumber
has six cases we can choose from:
So naturally, we tried to plug these in, using their symbol names directly:
But that didn’t work.
Back to digging. If we look at the implementation of Morphology.GrammaticalNumber
, we can see that it has a custom Codable
implementation, specifying other names for each of these cases:
Assuming that Codable
is used to read and write morphology
(see MarkdownDecodableAttributedStringKey
), we tried again using the encoded representation of plural
, which is "other"
:
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:
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:
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.GrammaticalNumber
), we can make some nice extensions out of this, starting with converting our known Int
to a Morphology.GrammaticalNumber
, then switching over that instead of using if
statements like we did above.
Hmm… we have to ask ourselves some tricky questions here. What’s the difference between plural
and pluralMany
? Is pluralFew
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 pluralFew
A small number of persons or things, as used for a grammatical number.case pluralMany
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:
The categories exactly match the cases available on Morphology.GrammaticalNumber
, 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?
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?
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:
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:
We feel comfortable using this only if specific conditions are met and tested:
- We know the user will never have a negative number of messages.
- We know that the UI is not shown when there are zero new messages.
- 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?
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.
Then, future use cases can be simplified:
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
- While we can’t find specific documentation pointing to the encoded
morphology
structure to support the use case here, we don’t believe it to be considered usage of undocumented APIs in the “reject your app” kind of way. It’s just not documented well enough to infer the behavior discussed in this post. Attributes likeinflect
are commonly used in shipping apps and appear in WWDC sessions, andmorphology: { partOfSpeech: \"noun\" }
appears in Apple’s sample code. You can even create your own attributes to use in markdown syntax usingMarkdownDecodableAttributedStringKey
. - While the behavior seen when using the
Morphology.GrammaticalNumber.zero
was initially unexpected, given what we learned about CLDR, we don’t think this is a bug. - These morphology features are not supported in every language. Initially, only English and Spanish were supported. According to WWDC23: Unlock the power of grammatical agreement, the list of supported locales now also includes French, Italian, Brazilian Portuguese, European Portuguese, and German. If your app supports only a subset of these, and you put in adequate testing time, you can consider using this solution.
- Automatic grammar agreement isn’t the only solution to pluralizing strings. String catalogs provide a robust solution for pluralization that’s compatible with far more languages.
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:
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.