Localize Interpolated String: Why Only String Value Work?

I found this thread by @Daniel_Hopfl
from 2016: Localization support for string interpolation

Using the example there, I can localize the interpolated string in SwiftUI.Text(), but only if the interpolated value is a String. Change to Int doesn't work.

The example:

let quote = "Never trust the internet!"
let person = "Albert Einstein"
SwiftUI.Text("<\(quote)> by <\(person)>")

should look up the key:

"<%@> by <%@>" = "%2$@ said: “%1$@”";

But if I simply change quote to an Int:

let quote = 1234

It stops working. It's as if the key doesn't match, no translation found.

I'm quite sure somehow this should work according to WWDC 2019 "What's New in Swift" What's New in Swift - WWDC19 - Videos - Apple Developer

at about 13:00 she talks about localizing interpolated string....

Anyone know something about this?

Edit: She actually start talking about localizing interpolated string at 22:15

I believe the reason is that %@ is the specifier for an object, of which String is one. You need a different specifier for ints.

1 Like

Yes, you are right. For Int she uses %lld. But how to indicate position in the translation? Just Something simple:

"number \(n), name \(s)"

.strings:

"number %lld, name %@" = "數字%lld、名字%@"; <-- works
"number %lld, name %@" = "名字%@、數字%lld"; <-- don't work

So @Daniel_Hopfl proposal and Apple's work not that same at all, no relationship? In @Daniel_Hopfl's sample implementation at GitHub - dhoepfl/DHLocalizedString: This is an idea on how to implement NSLocalizedString string localization in Swift., at the very end:

In your Localizable.strings file, replace all interpolations with %@ , regardless of the type of the interpolation.

This is better to me because the result of any interpolation is String, right?

I cannot test it but I guess the following should work:

"number %lld, name %@" = "名字%2$@、數字%1$lld"

(Note 2$ and 1$)

Actually I had to do it that way because back then I did not find any way to get the type of the interpolation. Interpolation got some big changes with Swift 5, so one can get the type now. In turn, this allows to have things like “%7.3lld” as translated format. And string dictionary need to get the raw value, IIRC (“No files“, “One file”, “%lld files”, …).

On the other hand, I’m not sure what happens if you compile for 32 bit: Is Int translated to “%lld”, “%ld”, or “%d”? Do we need different translation entries for each native word size?

1 Like

Yes, it works. I found the specifiers and positional indicators describe here: String Format Specifiers

Good to know...

I wonder why for SwiftUI.Text(), does Apple still use %lld, %f specifier? Could they not just let the "interpolator" output localized formatted string value? Is it because they need to pass thing down so they can do singular/plural case and the like? Seems with so many "Formatter" available, we can use them in our ExpressibleByStringInterpolation to do our localize formatting and not have to deal with %lld, %f , just use %@, seem more Swift this way.

I'm sure I haven't thought of all the requirements for this...things many not work as I imagine.

Thank you for helping me...

Edit: after thinking a little more. I believe Apple did it their way just to use existing localization mechanism.

SwiftUI needs to generate a string with format specifier so that it does work with the current localization mechanisms that exist on Apple's platforms and it does so by leveraging ExpressibleByStringInterpolation.

This is especially important for the interoperability story because you might have an existing localized string file and you want to use it in SwiftUI; but it's also very much important to use the tools that allows you to produce correct localization in multiple languages; for example, by supporting plural rules (via .stringdict) but also different positions in the sentence for the "variable"s. These are features that already exists and work well.

When you're interpolating a numeric value SwiftUI will choose a format specifier appropriate for that type. You have the ability to force a different format specifier:

Text("Guests: \(party.guests.count, specifier: "%d")").

Additionally, SwiftUI also supports formatters directly in the interpolation:

Text("Party Date: \(party.startDate, formatter: partyDateFormatter)").

This will run the localized string look up and then apply the formatter.

5 Likes

Thank you Luca!

What's the use case for specifying specifier? Wouldn't the type of the value determine what the specifier be? As your example:

Text("Guests: \(party.guests.count, specifier: "%d")").

count is a 64-bit Int so the specifier cannot be %d, should be %lld. Wonder why would we want to make specifier to something else?

Think of printing in hexadecimal or octal. Or specifying the number of decimal places for a floating point.

1 Like

It could also be useful as a general escape hatch, for when you want to represent things that Text doesn't yet have a high-level interface for, but which the underlying localization implementation supports.

1 Like

Can you give me an example? In looking at the source of LocalizedStringKey.StringInterpolation:

public mutating func appendInterpolation(_ string: String)

public mutating func appendInterpolation<Subject>(_ subject: Subject, formatter: Formatter? = nil) where Subject : ReferenceConvertible

public mutating func appendInterpolation<Subject>(_ subject: Subject, formatter: Formatter? = nil) where Subject : NSObject

public mutating func appendInterpolation<T>(_ value: T) where T : _FormatSpecifiable

public mutating func appendInterpolation<T>(_ value: T, specifier: String) where T : _FormatSpecifiable

So only _FormatSpecifiable can supply a specifier and _FormatSpecifiable is not public, these _FormatSpecifiable must be fixed, so regular user cannot add their own. Aren't they just whatever NSLocalizedString allow?

So only _FormatSpecifiable can supply a specifier and _FormatSpecifiable is not public, these _FormatSpecifiable must be fixed, so regular user cannot add their own. Aren't they just whatever NSLocalizedString allow?

Sorry to revive this old post but, in case anyone else ends up here trying to solve the same kind of problem, I'd like to explain something.

_FormatSpecifiable is a public protocol. It is however undocumented. (It's impossible to have a non-public protocol in a public API.)

The actual public protocol is:

public protocol _FormatSpecifiable : Swift.Equatable {
  associatedtype _Arg : Swift.CVarArg
  var _arg: Self._Arg { get }
  var _specifier: Swift.String { get }
}

It's found in SwiftUI's swiftinterface file:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64e-apple-ios.swiftinterface

You can see the public interface for how they have implemented it for stdlib types:

extension Int : SwiftUI._FormatSpecifiable {
  public var _arg: Swift.Int64 {
    get
  }
  public var _specifier: Swift.String {
    get
  }
  public typealias _Arg = Swift.Int64
}

Knowing this you could go into a playground and simply:

print(42._arg) // prints 42
print(42._specifier) // prints %lld
print(CGFloat(42)._arg) // prints 42.0
print(CGFloat(42)._specifier) // prints %lf

Etc.

However given that there is no documentation about this and its types have been marked with _ meaning, "for Apple's use only", I would recommend to not conform to this, or if you need to use it, then file developer support ticket and/or a feedback assistant issue to and see if we can ask them to make this public and document it.

Thanks for this new info.

I got sick of manually making up the right key. So I made this to get the key out the the Text:

extension Text {
    /// print out the LocalizedStringKey generated key
    /// for use in Localizable.strings and Localizable.stringsdict
    /// This string is the left hand side of a entry in Localizable.strings
    /// and top most <key>...</key> entry in .stringsdict
    /// Usage:
    /// ```
    ///    Text("My dog \("Paula") ate \(2) and \(0)")
    ///       .printLocalizedKey()
    /// ```
    @warn_unqualified_access
    func printLocalizedKey() -> Text {
        let mirror = Mirror(reflecting: self)
        if let key = mirror.descendant("storage", "anyTextStorage", "key", "key") {
            print(key as! String)
        } else {
            print("printLocalizedKey(): no key found!")
        }
        return self
    }
}

But I still have problem: the genstrings command line only recognize Text("string literal"). But these is a whole lot SwiftUI views that take LocalizedStringKey that are localizable. So I don't know of anyway to process my source to get all the localizable strings :frowning:

Not sure I understand.

Can you show an example of some functions that takes LocalizedStringKey where you're having the problem?

And what is the problem specifically? Is it that the genstrings command doesn't recognize these various places where you are using a custom localized string key as the input to a SwiftUI view?

genstrings is useless for SwiftUI currently. Two problems with the genstrings command:

  1. It generates wrong key. For this source:
Text("My dog \(name) ate \(percent * 100, specifier: "%.2f")% of \(count) bags")

genstrings generates this key:

"My dog %@ ate %@ and %@ bags" = "My dog %1$@ ate %2$@ and %3$@ bags";

SwiftUI.Text expects this key:

My dog %@ ate %.2f%% of %lld bags
  1. There are lots of view init's and accessibility that takes LocalizedStringKey but genstrings doesn't recognize. I've created a test with all of these init's. Every string literals that has LocalizedStringKey in it are LocalizedStringKey.

run genstrings on this input to see the problems.

import SwiftUI

extension Text {
    /// print out the LocalizedStringKey generated key
    /// for use in Localizable.strings and Localizable.stringsdict
    /// This string is the left hand side of an entry in Localizable.strings
    /// and top most <key>...</key> entry in .stringsdict
    /// Usage:
    /// ```
    ///    Text("My dog \("Paula") ate \(2) and \(0)")
    ///       .printLocalizedKey()
    /// ```
    @warn_unqualified_access
    func printLocalizedKey() -> Text {
        let mirror = Mirror(reflecting: self)
        if let key = mirror.descendant("storage", "anyTextStorage", "key", "key") {
            print(key as! String)
        } else {
            print("printLocalizedKey(): no key found!")
        }
        return self
    }
}

// here in contains may things that takes a `LocalizedStringKey` that can be localized
// but genstrings doesn't recognize at all
struct ContentView: View {
    let name = "Paula"
    let percent = 0.90
    let count = 2

    @State var color = Color.green
    @State var date = Date()
    @State var picker = 0
    @State var text = ""
    @State var flag = true

    var body: some View {
        VStack {
            Group {
                // this is the correct key
                // My dog %@ ate %.2f%% of %lld bags
                // this is genstrings output:
                // "My dog %@ ate %@ and %@ bags" = "My dog %1$@ ate %2$@ and %3$@ bags";
                Text("My dog \(name) ate \(percent * 100, specifier: "%.2f")% of \(count) bags")
                    .printLocalizedKey()    // look in console to see what the key is for this Text
                ColorPicker("ColorPicker LocalizedStringKey", selection: $color)
                DatePicker("DatePicker LocalizedStringKey", selection: $date)
                DisclosureGroup("DisclosureGroup LocalizedStringKey") {
                    Text("Some Key")
                }
                // should genstrings recognize "ABC"?
                Image("ABC")  // The name of the image resource to lookup, as well as the localization key with which to label the image.
                Image("ABC", label: Text("LocalizedStringKey")) // genstrings should recognizes label
                Link("Link LocalizedStringKey", destination: URL(string: "https://swift.org")!)
                Menu("Menu LocalizedStringKey") {
                    Button("Some Key") { }
                }
            }
            Text("Some Key")
                .accessibilityLabel("accessibilityLabel LocalizedStringKey")
                .accessibilityValue("accessibilityValue LocalizedStringKey")
                .accessibilityHint("accessibilityHint LocalizedStringKey")
                .accessibilityInputLabels(["accessibilityInputLabels LocalizedStringKey"])
                .accessibilityAction(named: "accessibilityAction LocalizedStringKey", { })
                .help(".help LocalizedStringKey")
            Group {
                NavigationLink("NavigationLink LocalizedStringKey", destination: Text("somewhere"))
                Picker("Picker LocalizedStringKey", selection: $picker) {
                    Text("Some Key")
                }
                ProgressView("ProgressView LocalizedStringKey", value: 0.3)
                SecureField("SecureField LocalizedStringKey", text: $text)
                Stepper("Stepper LocalizedStringKey", onIncrement: nil, onDecrement: nil)
                TextField("TextField LocalizedStringKey", text: $text)
                Toggle("Toggle LocalizedStringKey", isOn: $flag)
            }
        }
        .navigationTitle("navigationTitle LocalizedStringKey")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}