SwiftUI automatic string localization question

Viewed 692

I've been working on a SwiftUI app's localization, and I faced a localization-related situation which I don't quite understand (given, I'm not too proficient in SwiftUI yet, to begin with)

As far as I understand, at least in iOS 14, SwiftUI pretty much automatically applies localization to all "normal" strings (as long as I have proper localization files set up - which I do). However, I have two instances of the same string literal - one gets automatic localization treatment. The other does not.

So here's the situation I'm trying to figure out.

I have the following code:

NavigationView {
  NavigationLink(destination: CalendarSettingsView()) {
    SettingsNavLinkView(label: "Calendar") // <- this doesn't get localized
  }
}

And SettingsNavLinkView is set up as the following (just the skeleton related to question):

struct SettingsNavLinkView: View {
  var label:String
  
  var body: some View {
    Text(label) // <- localized "Calendar" is expected to be passed here
  }
}

In addition, CalendarSettingsView defines its title as in:

struct CalendarSettingsView: View {
  var body: some View {
    ScrollView {
       //some code
    }
    .navigationBarTitle("Calendar", displayMode: .inline) // <- "Calendar" here does get localized
  }
}

I do have key entry for "Calendar" in my localization files.

What is happening (and what I don't understand) is for the SettingsNavLinkView(label: "Calendar") component, the "Calendar" is NOT getting localized, HOWEVER, for CalendarSettingsView component (and related use case: .navigationBarTitle("Calendar", displayMode: .inline)) the "Calendar" string DOES get localized.

Both of these instances seem like the very same String to me, so I'm just trying to figure out what's going on here.

I did solve the issue by modifying the SettingsNavLinkView by specifically adding LocalizedStringKey initialization like below:

struct SettingsNavLinkView: View {
  var label:String
  let localizedLabel = LocalizedStringKey(label) // <-- NEW

  var body: some View {
    Text(localizedLabel) // <-- UPDATED to use localizedLabel instead of label
  }
}

But why did I have to do that? Why wasn't the "Calendar" string automatically localized at the point when it was passed to the SettingsNavLinkView as per this code SettingsNavLinkView(label: "Calendar")?

A bug in SwiftUI localization? My incomplete understanding of how localization works? I would prefer not having to resort to LocalizedStringKey for "simple strings"… But I'm not sure if what I'm asking for is even valid from the perspective of how "automatic" localization really works.

Any thoughts appreciated. Thanks!

1 Answers

Because different Text constructors are inferred for literal string and for variable string and that is documented in SwiftUI API

/// Creates a text view that displays a stored string without localization.
///
/// Use this intializer to create a text view that displays — without
/// localization — the text in a string variable.
///
///     Text(someString) // Displays the contents of `someString` without localization.
///
/// SwiftUI doesn't call the `init(_:)` method when you initialize a text
/// view with a string literal as the input. Instead, a string literal
/// triggers the ``Text/init(_:tableName:bundle:comment:)`` method — which
/// treats the input as a ``LocalizedStringKey`` instance — and attempts to
/// perform localization.
///
/// By default, SwiftUI assumes that you don't want to localize stored
/// strings, but if you do, you can first create a localized string key from
/// the value, and initialize the text view with that. Using a key as input
/// triggers the ``Text/init(_:tableName:bundle:comment:)`` method instead.
///
/// - Parameter content: The string value to display without localization.
public init<S>(_ content: S) where S : StringProtocol

as they said no localization.

but next with localization:

/// Creates a text view that displays localized content identified by a key.
///
/// Use this intializer to look for the `key` parameter in a localization
/// table and display the associated string value in the initialized text
/// view. If the initializer can't find the key in the table, or if no table
/// exists, the text view displays the string representation of the key
/// instead.
///
///     Text("pencil") // Localizes the key if possible, or displays "pencil" if not.
///
/// When you initialize a text view with a string literal, the view triggers
/// this initializer because it assumes you want the string localized, even
/// when you don't explicitly specify a table, as in the above example. If
/// you haven't provided localization for a particular string, you still get
/// reasonable behavior, because the initializer displays the key, which
/// typically contains the unlocalized string.
///
/// If you initialize a text view with a string variable rather than a
/// string literal, the view triggers the ``Text/init(_:)-9d1g4``
/// initializer instead, because it assumes that you don't want localization
/// in that case. If you do want to localize the value stored in a string
/// variable, you can choose to call the `init(_:tableName:bundle:comment:)`
/// initializer by first creating a ``LocalizedStringKey`` instance from the
/// string variable:
///
///     Text(LocalizedStringKey(someString)) // Localizes the contents of `someString`.
///
/// If you have a string literal that you don't want to localize, use the
/// ``Text/init(verbatim:)`` initializer instead.
///
/// - Parameters:
///   - key: The key for a string in the table identified by `tableName`.
///   - tableName: The name of the string table to search. If `nil`, use the
///     table in the `Localizable.strings` file.
///   - bundle: The bundle containing the strings file. If `nil`, use the
///     main bundle.
///   - comment: Contextual information about this key-value pair.
public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

I would recommend to use the following (tested with Xcode 12.1 / iOS 14.1)

struct SettingsNavLinkView: View {
  var label: String
  
  var body: some View {
    Text(LocalizedStringKey(label))     // << inline !!
  }
}
Related