Search code examples
iosstringlocalizationuikitios15

Taking advantage of the iOS 15 grammar agreement feature for English strings


New in iOS 15 there is a grammar agreement feature for English and Spanish. For example, I should be able to say

You have x apple.

And if x is 1, then "apple" is "apple", but if x is more than 1, then "apple" magically becomes "apples".

This sounds like a great way to get rid of all that code where I myself am looking at the number and choosing between hard-coded singular and plural options.

However, it looks to me like this feature is applicable only for localized apps, and only for attributed strings in UIKit. (SwiftUI is different because strings are localized automatically.) How do I do this for just a normal user-facing string?


Solution

  • You do have to use a localized attributed string in order to get this feature. But that doesn't mean the string you present has to be localized or attributed!

    Here's an example. At some point, we might say this:

    let count = 1
    let output = String(AttributedString(
        localized:"You have ^[\(count) \("apple")](inflect: true).").characters)
    print(output) // You have 1 apple.
    

    Now imagine that at some other point, we say this:

    let count = 2
    let output = String(AttributedString(
        localized:"You have ^[\(count) \("apple")](inflect: true).").characters)
    print(output) // You have 2 apples.
    

    Notice that my attributed string is the same in both. And yet, based on the count value, the runtime has changed "apple" to "apples" where needed. This is just what we wanted.

    So here's the deal, as far as I can tell.

    • You must use an attributed string, and it must be declared as localized (in UIKit).

    • As soon as you do that, you are allowed to use Markdown. You do use Markdown, using the special Apple-devised custom attribute syntax. In this way, the inflected region of the string is made visible to the runtime.

    • Everything relevant must be included in the square brackets section, and must be marked using string interpolation as I've shown it here. In this case, "everything relevant" is the count number and the noun. Use the singular for the noun; if you use the plural, it won't be inflected down to singular, but if you use the singular, it will be inflected up to plural.

    • The string itself must be a literal in order to get the special interpolation rules; in particular, it must be typed as a StringLocalizationKey (NOTE Subsequently renamed to String.LocalizationValue). I could construct the string this way:

      let count = 2
      let s : StringLocalizationKey = "You have ^[\(count) \("apple")](inflect: true)."
      // Later revised to: let s: String.LocalizationValue = ...
      

    That gives you the automatic inflection. Then, in this instance, because I just want the string, I pass through the characters view of the attributed string and coerce that to a string.

    Observe that my app is not localized in any meaningful sense. I have no other languages declared for the app, and I have no .strings files or .stringsDict files. I'm only passing through the localized: constructor because that's how you access this feature.