Localization in Xcode

Hi,

I‘m excited for the new localization features in Xcode 13. I learned something new about how it is expected to use Localization Strings.

Before watching the videos I‘d always use a unique key for localizations, such as "navigationbar_leadingButtons_back" = "Back" and used them in my code, which made it a bit tricky to read them, instead of how it is shown in this WWDC by just using the english text "Back" and use "Back" as a key.

However, if I have a longer sentence, let‘s suppose a description of a feature that has several lines.

"This feature provides the ability to do xyz"

If this is used as a Localization Key, then changing the text (because of changes of the feature or another reason) creates a new key, which breaks most of the translations that have already been done, since the key is gone right? If I export Localizations again now, then already translated strings for the old key are dropped, aren‘t they?

I wonder why it is not advised to use developer friendly keys, that don‘t change and only change the text if needed?

Additionally neither strings, nor developer-friendly strings as keys support compile time existence checks as far as I understand. Of course there are fallbacks to other languages, but it doesn‘t tell you unless you try out the app in that language. That‘s why I‘m still using developer-friendly keys with underscores in order to generate Swift Code from them to be used in my app, which gives some compile time check opportunities, as you can‘t access the localization if there is no key for it.

How would the problem of renaming a key, by changing text in your app be handled in Xcode‘s Localization generator? And what are good reasons besides legibility for developers only for using complete texts as keys?

E.g Compare:

"This purchase gives you (fuelCount) fuel for your car"

to

Localization.Store.Products.Fuel.purchaseText.text(with: "\(fuelCount)")

And this code represents "store_products_fuel_purchaseText" = "This purchase gives you {0} fuel for your car";

The key would never need to change, if we want to change the text as it is not the same key.

Thanks for any info in advance 😃

Replies

This is a lot of questions — I’ll do my best to answer!

  • Should I use "KEYS_LIKE_THIS" or "Keys like this?"
  • Does changing a key cause loss of translations?
  • Can missing localisations be a compile-time (instead of runtime) error?

Should I use "KEYS_LIKE_THIS" or "Keys like this?"

Actually, it doesn’t matter! Both will work, and this is more of a matter of personal preference than best practices. Personally, I think using the English string as the key is actually more developer-friendly than SCREAMING_SNAKE_CASE because it helps you visualise the strings in the context of your app.

Does changing a key cause loss of translations?

It depends!

You may think that a localisable string’s key is what uniquely identifies it for translation, but it turns out there are many more contextual clues when it comes to determining “which” string you’re dealing with. Third-party translation tools can use other bits of information such as file path and the source string contents to match up the target strings they have in their translation memory with what’s being sent over from your project. If the key changes but everything else remains the same, a good translation management system will be able to leverage past translations automatically — without requiring any extra work from the translators.

On the flip side of this, let’s say you’re using an English string as the key. If the string changes substantially, that often means the translation should too!

Can missing localisations be a compile-time (instead of runtime) error?

This is an interesting question. You can get part of the way there with the static analyser. In your Xcode Project, on the Build Settings tab (enabling “All” if only “Basic” is shown), search for “Missing Localizability” and set this to Yes. Now when you build or analyse your project, the compiler will point out places where you’ve forgotten to use localised string APIs.

That won’t catch strings missing from strings files, though. For this, you should see a warning when going through the “Product > Import Localisations…” flow. If that still doesn’t do the trick for you, and you don’t want to rely on manual runtime loc QA, feel free to report a bug via Feedback Assistant (being sure to post the FB# here)!

  • Thank you for this extensive reply! You are right, that a natural english sentence is probably easier to read, but it also doesn‘t give away that it has been localized. What Xcode 13 does, which is powerful, is essentially creating the Localizable strings for you based on your code, which saves a lot of time. However, what I was looking for and what I‘m currently using (hence the snake case keys) is the other way around: At build time, generate Swift Code based on the Localizable.strings and then use Swift code (basically a bunch of nested enums) to access and map the key in Swift. This still doesn‘t help me cover missed translations in other languages but English, but I think that is a powerful way of dealing with Localizations.

    I‘ll definitely check out the missing localization-api build settings. Thanks!

    Over the next few days I‘ll also draft out a suggestion and submit feedback for it. Will post the feedback id here then.

  • If you add a comment for the localiser on every string (which is a best practice — does “Book” refer to a book in a bookshelf, or booking a hotel?), it will be clear that your strings are indeed localised :D. Can you tell me a bit more about what you’re trying to do though? You should be able to simply use the localized string API, without any need for wrapper types: NSLocalizedString("Book", comment: "Button title for confirming a hotel trip")

  • Sorry for my late reply. I did not get a notification. I am trying to map all my localizations in code. This means that I access code for accessing localization strings (which internally uses NSLocalizedString). This could be something like: Text(Localization.Bookshelf.Book.text) or Localization.SomeCategory.SomeSection.SomeKey.text. This way it is very descriptive: It tells you the path to the localization, you can query it easily using code completion and internally it translates to: NSLocalizedString("bookshelf_book") or NSLocalizedString(someCategory_someSection_someKey). It also prevents me from using a Localization that no longer exists or has changed, because it would result in a compile time error. Additionally it offers support for other NSLocalizedString parameters: Text(Localization.Bookshelf.Book(comment: "", tableName: "").text. Using the english text does not really work for me for multiple reasons as specified by others below, and you don't get compile time checks for something that is available at compile time.

Add a Comment

I was surprised from this too when I saw the Apple examples not using keys, ever since SwiftUI was released and even in Xcode 13. I've always used strongly-typed keys in production apps because changing the text would break all my translations.

I'm really trying to see how Apple sees it and use the English translations right in the view as the keys. I even refactored one of my production apps like this to get a true sense. Translation breaks much more frequently on copy changes, but I can see the argument that it's expected since the other languages should change too, and importing them gives the warning to catch those that break, although a compiler warning would be nice because Missing Localizability never really catches anything because LocalizedStringKey conforms to ExpressibleByStringLiteral so Xcode will always assume you're using a localized entry.

The other thing from here that's hard to accept is the localization comments crowding up the views badly. I've wondered why the localization comments and even table names weren't applied to LocalizedStringKey instead of the Text view so I can do something like below which seems to be the best of both worlds while also keeping views succinct. It also allows me to distribute and pass around the localization entry even in places that don't accept a Text view (so I don't have to make a Label the long way for example):

extension LocalizedStringKey {
    static let fuelPurchase = LocalizedStringKey(
        "This purchase gives you fuel for your car",
        tableName: "StoreProducts",
        bundle: .storeProducts,
        comments: "This is the text that's displayed for fuel purchases"
    )
}

Text(.fuelPurchase)
Label(.fuelPurchase, systemImage: "bell")

I submitted this feedback in #FB9169281 but wanted to share here to get thoughts from others and if maybe there's something I'm missing.

PS - I'm not sure if the Apple team has examined how Android Studio handles localizations but it has always been a dream. It allows you to right click on English words and extract to localization. It uses keys behind the scenes to keep all the languages in sync, but during development time, the real English value get super imposed in the editor. So even in my example, I would see Text("This purchase gives you fuel for your car") in the editor based on my dev environment, but clicking on it would show the true code of Text(.fuelPurchase).

Discussed this more with cross-functional teams and not using keys a no-go. This will not work if you want to share localization efforts across platforms beyond Apple which is more of a real-world scenario especially in enterprise settings. For example, Android requires localization keys to not have dots and other kinds of characters, also we found many 3rd party localization services had a character limit on key length. And when looking at very long localization entries, using the value as the key just doesn't make sense. I think sticking with my first intuition in using localization keys is the right way despite all the Apple sessions and samples showing otherwise.

Also, just to share I was able to achieve isolating localization configurations by extending Text views to initialize from other Text views. This way, I can offload the localization configurations off the view by creating a bunch of static Text views and let the rest of the app pick them off the shelf.

First I create the simple but awkward Text extension:

public extension Text {
    /// Creates a text view from another text view for isolating localization entries.
    init(_ text: Text) {
        self = text
    }
}

Then create the static Text views with all localization configurations:

extension Text {
    static let notificationTitle = Text(
        "Notifications",
        tableName: "NotificationSettings",
        comment: "The section title for notifications group on the settings screen"
    )

    static let snoozeTime = Text(
        "Snooze time",
        tableName: "NotificationSettings",
        comment: "The title of the snooze time in the notifications section of the settings screen"
    )

    static let snoozeHelp = Text(
        "The number of minutes to remind you again later for action, like the functionality of an alarm clock.",
        tableName: "NotificationSettings",
        comment: "This is the comment for this localized entry"
    )
}

Then elsewhere in a view, I can do this:

struct ContentView: View {
    var body: some View {
        VStack {
            Text(.snoozeTime)
            Text(.snoozeHelp)
        }
        .navigationTitle(.notificationTitle)
    }
}

This still lets the localization to be exported properly since the static Text views aren't dynamically generated from variables. I still wish the localization configurations weren't in the Text view itself but in the LocalizedStringKey or a new type such as LocalizedString, since this is the way that String(localized:table:bundle:locale:comments:) works and would just need a SwiftUI counterpart to it.