Skip to content

Text(_:) StringProtocol overload localizes its argument instead of displaying it verbatim #135

Description

@vincentborko

Text.init<S>(_ content: S) where S : StringProtocol localizes its argument. SwiftUI documents that overload as displaying "a stored string without localization" — it is the verbatim path, which is what a String variable binds to. Text(verbatim:) and Text(_ key: LocalizedStringKey) both behave correctly; only this overload diverges.

Sources/SkipSwiftUI/Text/Text.swift:22:

@_disfavoredOverload public init<S>(_ content: S) where S : StringProtocol {
    self.init(spec: TextSpec(key: LocalizedStringKey(String(content))))
}

LocalizedStringKey(_:) forwards to init(stringLiteral:), so the string becomes a lookup key and SkipUI._Text resolves it against EnvironmentValues.shared.locale.

Reproduction

Localizable.xcstrings with base language en, key Welcome, fr translation Bienvenue:

let resolved: String = "Welcome"   // a String variable, not a literal

var body: some View {
    VStack {
        Text(resolved)             // StringProtocol overload
        Text(verbatim: resolved)
        Text("Welcome")            // literal -> LocalizedStringKey
    }
    .environment(\.locale, Locale(identifier: "fr"))
}
Skip Fuse / Android SwiftUI
Text(resolved) Bienvenue Welcome
Text(verbatim: resolved) Welcome Welcome
Text("Welcome") Bienvenue Bienvenue

Measured on an Android emulator (API 36).

Why it matters

An app that resolves its own strings — for an in-app language setting independent of the device language, for instance — hands Text a finished string. If that string is also a key in the catalog (which it is whenever the catalog's source language is the app's own, since then the keys are the source strings), it gets translated a second time, into whatever \.locale happens to be. The result is a screen rendered in two languages, and it is invisible on iOS.

Text(verbatim:) is the workaround, but it only works where the author knew to reach for it: on iOS the plain call is already verbatim, so nothing signals that the Android build differs.

Note that Skip Lite is a separate case and not what this is about: SkipUI.Text has a non-generic init(_ key: String) that intentionally treats a String as a key, because after transpilation to Kotlin a literal and a variable are indistinguishable at the call site (CHANGELOG, 2023-12-22). Skip Fuse compiles with real Swift overload resolution, so the distinction is available here and the SwiftUI contract can be honored.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions