Localizing plurals when the number is not included in the text

153 Views Asked by At

What is the correct way to localize and pluralize a string when the number is not included in the text?

I know I can do the following with AttributedString (already a great feature):

var count = 3
AttributedString(localized: "^[\(count) Chief-of-staff](inflect: true)")
// "3 Chiefs-of-staff"

I want to list names in a table/list, and use either "Chief-of-staff" or "Chiefs-of-staff" for the list header, localized and as appropriate for the number of names in the list.

Following the excellent article here, this use-case looks analogous to the existing implementations of InflectionConcept. Maybe it needs a new count inflection concept to be implemented. One might then be able to do something like this (pseudo-code, does not work!):

var options = AttributedString.LocalizationOptions()
options.concepts = [.count(count)]

AttributedString(localized: "^[Chief-of-staff](agreeWithConcept: 1)", options: options)

The only progress I have made is to use Morphology directly, but this fails because the logic for determining GrammaticalNumber is locale-specific. The following works in English and other languages that only have two forms (singular and plural), but not in languages that use pluralTwo, pluralFew, pluralMary, and so on.

extension Morphology.GrammaticalNumber {
    init(count: Int) {
        switch count {
        case 1:     self = .singular
        default:    self = .plural
        }
    }
}

extension LocalizedStringResource.StringInterpolation {
    mutating func appendInterpolation(_ input: String, count: Int) {
        var morphology = Morphology()
        morphology.number = .init(count: count)
        
        var string = AttributedString(input)
        string.inflect = InflectionRule(morphology: morphology)
        
        appendInterpolation(string.inflected())
    }
}

AttributedString(localized: "\("Chief-of-staff", count: count)")

One possibility is to use the number in the string and then strip it out, but this feels like a horrible hack:

var header = AttributedString(localized: "^[\(count) Chief-of-staff](inflect: true)")
let range = header.range(of: "\(count) ")!
header.removeSubrange(range)
// Chief-of-staff

Am I missing something obvious, or is there a neater solution?

1

There are 1 best solutions below

0
Ken Cooper On

While I agree it's a hack, here's at least some packaging to hide the mess using your solution:

extension String {
    func pluralize() -> String {
        let count = 2
        var attributed = AttributedString(localized: "^[\(count) \(self)](inflect: true)")
        let range = attributed.range(of: "\(count) ")!
        attributed.removeSubrange(range)
        return String(attributed.characters)
    }
}