Search code examples
iosswiftstringswiftuinslocalizedstring

How to use interpolation with strings that rely on automatic grammar agreement?


I'm trying to solve a seemingly simple issue: display free trial terms to the user in SwiftUI Text view. I have a unit string set to "Week" and a value that I can switch from 1 to 2. To handle plurals, I'm taking advantage of automatic grammar agreement on iOS 15. I have a string ready in my Localizable.strings file like so:

"Subscription period: %lld %@" = "^[%lld ^[%@](grammar: { partOfSpeech: \"noun\" })](inflect: true)";

To build out the string, I need to use string interpolation to add the word 'free'. I know that adding the word inside strings file will solve the issue, but I don't want to do that. I want to use that string universally, not necessarily pertaining to free trials. I don't understand why I can't call some init, pass it a key, and get a plain string back that I can interpolate as I wish. I've tried a lot of ways that are showcased in the code snippet attached. Either the string is not found, or the automatic grammar agreement is not applied. There appears to be String.LocalizationValue.StringInterpolation struct, but I can't figure out how to use it since there is no documentation whatsoever. What am I doing wrong? I would appreciate any help!

struct ContentView: View {
    /// This value mimics what **StoreKit 2** returns for `Product.SubscriptionPeriod.Unit` in its `localizedDescription` on iOS 15.4.
    let unit = "Week"
    
    @State var value = 1
    
    var key: LocalizedStringKey {
        "Subscription period: \(value) \(unit)"
    }
    
    var plainStringKey: String {
        "Subscription period: \(value) \(unit)"
    }
    
    var body: some View {
        Form {
            Section {
                // Works fine without string interpolation.
                Text(key)
                    .foregroundColor(.green)
                
                // `NSLocalizedString` doesn't work and it shouldn't as per its docs.
                Text("\(NSLocalizedString(plainStringKey, comment: "")) free")
                    .foregroundColor(.red)
                
                // Doesn't see the string.
                Text("\(String(localized: String.LocalizationValue(stringLiteral: plainStringKey))) free")
                    .foregroundColor(.red)
                
                // This way also doesn't see the string, which is strange, because it should be just like the following way, which does see the string.
                Text("\(String(localized: String.LocalizationValue.init(plainStringKey))) free")
                    .foregroundColor(.red)
                
                // Sees the string (only if I pass literal, not the plainStringKey property), but doesn't apply automatic grammar agreement.
                Text("\(String(localized: String.LocalizationValue("Subscription period: \(value) \(unit)"))) free")
                    .foregroundColor(.red)
                
                
                // MARK: Bad solution:
                /*
                 - Doesn't work with Dynamic Type properly
                 - You have to approximate horizontal spacing
                 */
                
                HStack(spacing: 3) {
                    Text("Subscription period: \(value) \(unit)")
                        .textCase(.lowercase)
                    Text("free")
                }
                .foregroundColor(.orange)
            }
            
            Section {
                Stepper(value: $value, in: 1...2) {
                    Text("Value: \(value)")
                }
            }
        }
    }
}

You can download the sample project here.


Solution

  • I was actually very close with this piece of code:

    Text("\(String(localized: String.LocalizationValue("Subscription period: \(value) \(unit)"))) free")
    

    But using String(localized:) isn't correct. I guess it's because automatic grammar agreement uses markdown, which is the prerogative of AttributedString. We should use AttributedString(localized:). But even then, it's not going to inflect because apparently there is a bug in Foundation. But once it gets patched, it will work like a charm.

    Here is the correct solution:

    func freeTrialDuration() -> Text {
        // Use plain string.
        let duration: String = "^[\(value) ^[\(unit.lowercased())](grammar: { partOfSpeech: \"noun\" })](inflect: true)"
        // Use the following way to get the localized string.
        let localizedDuration = AttributedString(localized: String.LocalizationValue(duration))
        
        return Text("\(localizedDuration) free")
    }