SwiftUI: Text not localizing interpolation when casting to LocalizedStringKey

Hello! :wave:t3:

I have a quick question about Text in SwiftUI:

As described in Apple's documentation, there are two particular initializers used when passing in string literals or stored strings. I will use markdown to explain:

Perhaps this is a hidden detail a few people might not know about when using Text in their SwiftUI projects. If you wanted to use a stored string with localization, you could convert your string to a LocalizedStringKey and call the first initializer listed above:

let content = "**Hello!**"
Text(LocalizedStringKey(content)) // Localization used βœ…

Output: Hello!

My question revolves around this and string interpolation. Text can take advantage of passing in different types with string interpolation, which can be really useful at times. However, when interpolating different types, I seem to have results that do not match up with my above markdown example. I will use SF Symbols to explain:

Those outputs are expected. The part where I get confused is here:

let content = "**Hello!** \(Image(systemName: "hand.wave"))"
Text(LocalizedStringKey(content)) // Markdown works, but the SF Symbol does not ❓

Output: Hello! Image(provider: SwiftUl.ImageProviderBox<SwiftUI.Image.NamedlmageProvider>)

I am casting content to a LocalizedStringKey, but only a portion of it seems to be localized.

This isn't a difficult problem to solve. I can just use type annotation to get the desired output:

let content: LocalizedStringKey = "**Hello!** \(Image(systemName: "hand.wave"))"
Text(content) // Localization used βœ…

Output: Hello

However, this isn't ideal. In certain contexts, I would really like to directly cast inside the Text initializer. I have tested this with SwiftUI's Image struct using SF Symbols and images from the asset catalog, and I get the same result.

What am I misunderstanding here? What can I clarify for anyone? I really appreciate everyone's help! :slightly_smiling_face:

Edit: I apologize if this isn't the best place to post this, but I cannot tell if this is intended behavior or not.

– Joe

This happens because there is a special appendInterpolation for Image that it's only used when creating a LocalizedStringKey.

1 Like

Hello! Thanks for the response, I had those exact docs open before I posted this. :man_facepalming:t3:

The struct LocalizedStringKey.StringInterpolation is described:

Represents the contents of a string literal with interpolations while it’s being built, for use in creating a localized string key.

Thus, it looks like the appropriate appendInterpolation methods are called only when creating a localized string key.

However, I am creating a localized string key, just directly in Text. I have carefully read through the documentation once again, and I must be missing something. Particularly, I took a second look at the documentation for LocalizedStringKey and seem to have problems with how I am passing strings into one β€” the behavior kind of looks similar to how you pass in string literals compared to stored strings with Text.

I was playing around with it in an app playground, and found these interesting properties:

Just like before:

I then altered this and put the string literal of content directly into the localized string key:

Text(LocalizedStringKey("**Hello!** \(Image(systemName: "hand.wave"))")) // Localization used βœ…

Output: Hello

The output changed depending upon if I put a string literal or stored string into a LocalizedStringKey. I know this is the defined behavior for the two Text initializers I mentioned in the original post, but I don't believe there is anything mentioned like this for LocalizedStringKey itself.

I then continued to play around and found this, which was especially odd:

Text(LocalizedStringKey("**Hello!** \(Image(systemName: "hand.wave"))"))

Output: Hello

Text(LocalizedStringKey.init("**Hello!** \(Image(systemName: "hand.wave"))"))

Output: Hello! Image(provider: SwiftUl.ImageProviderBox<SwiftUI.Image.NamedlmageProvider>)

The only difference between those two calls is the use of .init() explicitly.

I understand what the documentation states, but I do not understand these little details.

Edit: Perhaps the appendInterpolation method (in this case for Image) is only called when you use a string literal in a LocalizedStringKey. The initializer for LocalizedStringKey.StringInterpolation is described:

Creates an empty instance ready to be filled with string literal content.

The discussion states:

Don’t call this initializer directly. Instead, initialize a variable or constant using a string literal with interpolated expressions.

I guess when they say string literal, they really mean string literal? I would have just thought you would accept any type of string.

This has nothing to do specifically with Text or LocalizedStringKey; it is a general applicable aspect of the Swift language that applies to any type expressible by any literal.

It is important to understand that an integer literal is not an Int and a string literal is not a String. When a type conforms to an ExpressibleBy* protocol (such as ExpressibleByIntegerLiteral or ExpressibleByStringInterpolation), it gets to initialize a value from that literal in any way it chooses.

This is why, even though the default integer literal type is Int, a type such as Double can be initialized from an integer literal larger than Int.max: Double handles initialization from the literal directly which is at no point represented as an Int value.

Similarly, even though the default dictionary literal type is Dictionary, which requires unique keys, KeyValuePairs and other types that conform to ExpressibleByDictionaryLiteral need have no such limitation: again, they handle initialization from the literal directly which is at no point represented as a Dictionary value.

In Swift, if the context in which you write a literal expression demands a non-default literal type, then you can just write the bare literal:

func f(_: KeyValuePairs<Int, String>) { ... }
// Fine, because the argument isn't (ever) and can never be a `Dictionary`:
f([42: "Hello", 42: "World"])

// Not fine at all, because `x` is a `Dictionary`:
let x = [42: "Hello", 42: "World"]
// f(x)

let y = [21: "Hello", 42: "World"]
// Not fine even if you fix the duplicate keys, 
// because there is no `f` that takes a `Dictionary`:
f(y) 

In the absence of context, to initialize a value of non-default literal type from a literal, there were originally two spellings available:

let a: Double = 9223372036854775808
let a = 9223372036854775808 as Double

However, no matter how many times we taught users otherwise, they'd write the following instead:

let a = Double(9223372036854775808)

Until SE-0213, what this actually meant was: create a value of type Int, then convert the value to Double. As a result, it wouldn't compile because 9223372036854775808 > Int.max, and folks would be confused. So, we changed the language to match user expectations. Now, the last code block above is another way of expressing 9223372036854775808 as Double.

After SE-0213, we still needed to preserve a spelling to opt into calling the actual initializer (for example, to convert from Int to Double), so to do so we now require writing out .init. That way, if you explicitly want to convert Double from an Int value initialized from a literal, you can write:

let b = Double.init(42) // equivalent to:
let b = Double(42 as Int)
1 Like

Did you try issuing a compiler diagnostics upon users with a fix-it "perhaps you meant 9223372036854775808 as Double or just 9223372036854775808.0 ?"

That Double.init(...) is not the same as Double(...) sounds quite strange.

2 Likes