Radu Dan
AI-generated visual showing how tricky working with Localizations in Swift can be

Cracking the Localizable Strings in iOS

In this article, we will explore the modern approach to localizing strings in an iOS application. We will delve into Apple's Foundation frameworks related to localization and learn how to effectively use them.

The source code for this article is open source and available on GitHub. You can also read more about this topic on [Medium]().

Motivation

Recently, I have been working on an iOS application and sought an easy way to make it accessible to a global audience. While SwiftUI simplifies the process, I found myself puzzled by the various methods available for localizing text.

The Fun Way to Work with Strings

It all started simply. In my Localizable.xcstrings catalog, I have a key named main.onboarding.title.label, which I plan to use in a Text view on my main screen.

Strings catalog from Xcode

The main screen is built using SwiftUI and features a straightforward card containing an Image and a Text view.

struct MainView: View {
    var body: some View {
        VStack {
            Image(systemName: "sun.max.fill")
            Text("main.onboarding.title.label")
            ...
        }
        ...
    }
}

When I build the project, I automatically receive the key main.onboarding.title.label, as shown in the previous image.

So far, so good. I can easily navigate to my Strings catalog, add a new language, and select French.

Add a new language (French) to my Strings catalog

This process is seamless; you don't need to worry about the underlying mechanics. You can add your key in the UI element, build the project, go to your Strings catalog, translate it, and you're done.

Here is the complete main screen of the app displayed in both French and English languages.

Understanding Localization is Hard

As an iOS developer, you may encounter various methods for translating text in larger projects.

In one project, you might see this:

let title = LocalizedStringResource(stringLiteral: "weird.text")

In another project, you could come across:

let petDetailsScreenTitleKey = LocalizedStringKey("dog.details.title")

Or this:

let alertTitle = String(localized: "login.alert.title.label")

In older projects, you might find strings localized like this:

let commentBoxText = NSLocalizedString(
    "comment.box",
    comment: "Users enter their comment in this box."
)

And one more example:

let placeholderLocalizationValue = String.LocalizationValue("textfield.placeholder")

That's too much. What are these functions and classes doing to my lovely String? Is my String even a String anymore or it lost the authenticity?! Who knows?!

Cracking the Code

Don't worry; things will become clearer shortly.

Let's apply all of these localization methods to our key defined at the beginning of the article:

static let localizationValue = String.LocalizationValue("main.onboarding.title.label")
static let localizationStaticString: StaticString = "main.onboarding.title.label"

let localizedResourceWithStringLiteral = LocalizedStringResource(
    stringLiteral: "main.onboarding.title.label"
)

let localizedResourceWithLocalizationValue = LocalizedStringResource(localizationValue)

let localizedResourceWithStaticString = LocalizedStringResource(
    localizationStaticString,
    defaultValue: localizationValue
)

let localizedString = String(localized: "main.onboarding.title.label")

let localizedStringKey = LocalizedStringKey("main.onboarding.title.label")

let localizedStringOldWay = NSLocalizedString(
    "main.onboarding.title.label",
    comment: "Used to show the onboarding title."
)

Believe it or not, the first three variables — localizedResourceWithStringLiteral, localizedResourceWithLocalizationValue, and localizedResourceWithStaticString — are equivalent. They hold the same information and point to the same localized string resource, producing identical output when used in our app.

The same applies to localizedString and localizedStringOldWay, which will yield the translated version of your string.

Finally, using localizedStringKey in a SwiftUI Text view will also produce the translated string.

Now we understand that we have multiple ways to translate a string and present it to our users.

Let's clarify when to use each method.

LocalizedStringResource vs String(localized:)

According to Apple's documentation, LocalizedStringResource is a "reference to a localizable string, accessible from another process." We should use LocalizedStringResource when we want to provide localizable strings with lookups deferred to a later time.

What does this mean? Why would we want to defer lookups?

Here are a few scenarios:

  1. When communicating between processes (like the main app and extensions),
  2. When you want to change the locale before displaying the string,
  3. When working with features like Siri or App Intents.

Imagine a notification system that allows users to schedule messages for a later time. We want those messages to be in the user's preferred language, which might differ from the app's current language. For example, the app is in English, while the user's device has been changed later on to French.

struct NotificationScheduler {
    /// The localized greeting message key used in notifications.
    private static let greeting = LocalizedStringResource("main.onboarding.title.label")
    
    func scheduleNotification(forUserWithLocale locale: Locale) async {
        // Create a unique notification request with localized content and trigger.
        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: buildContent(locale: locale), // contains the `greeting` message
            trigger: buildTrigger() // creates the `UNTimeIntervalNotificationTrigger` object
        )

        // Get the shared notification center instance for managing notifications
        let notificationCenter = UNUserNotificationCenter.current()
        
        // Request user's permission to show notifications
        await requestAuthorization(notificationCenter: notificationCenter)
        
        // Schedule the notification with the notification center
        await scheduleNotification(
            request: request,
            notificationCenter: notificationCenter
        )
    }
    ...
}

We can trigger the notification scheduler with a Task, as the code is asynchronous:

Task {
    await scheduler.scheduleNotification(
        forUserWithLocale: Locale.current
    )
}

This ensures that the notification displays the correct translation based on the device's locale.

Understanding this concept is crucial. If we had used String(localized: "main.onboarding.title.label") when setting the notification content, the message would have already been translated. If the user changed the locale or language before receiving the scheduled notification, they would see it in a different language.

This is shown in the below image, where the message of the notification is in English, but the phone is in French:

Banner's message is in English while the phone is in French

Seeing a real-world example applied to an app helps clarify this important concept.

Moreover, Apple states that LocalizedStringResource is the recommended type for representing and passing around localizable strings. If you need to provide localized strings to another process that might be using a different locale, then LocalizedStringResource is the way to go.

In summary, this type is part of Apple's efforts to streamline localization in iOS applications, making it easier for developers to create apps that are accessible to a global audience.

What About String.LocalizationValue?

Apple recommends using "this type when the localization key is the localized string value in the development language." This essentially means that the string you use as a key is the same string that will appear in your default development language (usually English).

For example, consider the localization key "Sign In," with translations in different languages:

  • 🇬🇧 English: "Sign In",
  • 🇪🇸 Spanish: "Iniciar Sesión",
  • 🇫🇷 French: "Se Connecter".

One drawback is that String.LocalizationValue does not conform to StringProtocol, so you can't use it directly in SwiftUI Text views. As a key, you can use it with LocalizedStringResource(keyAndValue:) or with String(localized:) initializers, for example.

// ❌ This does not work
Text(String.LocalizationValue("main.onboarding.title.label"))

// ✅ This works
let localizationValue = String.LocalizationValue("main.onboarding.title.label")
Text(LocalizedStringResource(localizationValue))

// ✅ This also works
Text(String(localized: localizationValue))

How Is That Different from LocalizedStringKey?

LocalizedStringKey was integrated into SwiftUI from the beginning and works with many UI components and modifiers.

When you use this initializer, it searches for the key you provide in a localization table. If it finds the key, it displays the corresponding string in the text view. However, if the key isn't found or if there's no localization table available, the text view will simply show the key itself as a string.

Compared to String.LocalizationValue, a major advantage of LocalizedStringKey is that it can be used directly in SwiftUI views.

// ✅ This works
Text(LocalizedStringKey("main.onboarding.title.label"))

Oops, We Forgot NSLocalizedString

We can safely assume that when we see something that starts with NS, it's something old—specifically, Objective-C old. You are correct.

NSLocalizedString was introduced in iOS 2.0 🙀, 16 years ago, during the era of the iPhone 3G when most programmers used NSLog to print messages to their console.

It works perfectly well with SwiftUI and can be safely used in our codebase. In Swift, we use its successor, String(localized:).

// ✅ This works
Text(
    NSLocalizedString(
        "main.onboarding.title.label",
        comment: "Used to show the onboarding title." // comment for translators
    )
)

God Mode IDDQD

Consider the following challenge:

You are working on the next-gen iOS puzzle game and have been assigned an important task: designing the "Special Hint" feature.

What is the "Special Hint" feature, you might ask? Let me explain.

  1. Users will see a text field to enter a keyword.
  2. To progress to the next level, they must enter the correct keyword.
  3. They have opted in to receive special hints based on the entered words that are related to the keyword.
  4. If they enter a word that should trigger a special hint but is not the keyword, the game should display an alert with a localized message.

📒 Note:

  • The iOS puzzle game is localized in multiple languages.

Example:

Pierre has his device set to French and enters the word: 'morse'.

The word 'morse' is recognized as a trigger for a special hint.

The special hint in English is 'You will have to be more archaic.', which can be translated approximately in French as 'Vous devrez être plus archaïque.' The message in French is shown to Pierre.

Jane has her phone set to English and enters the word: 'walrus'. The special hint will be triggered, and Jane will see the message 'You will have to be more archaic.'

📒 Note: The game should have multiple special hints associated with the keyword that unlocks the next level (e.g., walrus, cone, banana). Each special hint will trigger a different message.

Before jumping to the solution, I encourage you to resolve this problem. It will help you better understand the localization concepts we discussed in this article.

Here is one way to approach the implementation:

  • The user is French and enters the special hint "morse."
  • We have a predefined array of keys: ["walrus", "banana", "cone"]. These keys should be localized based on the entered word.
  • Translations in French for those keys are: "walrus" = "morse"; "banana" = "banane"; "cone" = "cône".
  • For each key in the array, retrieve the localized string for the key. The mapped array will be ["morse", "banane", "cône"].
  • If one of the keys matches the special hint "morse", we should display the special hint message. To achieve this, we can create a convention for each key and add "_value" as a suffix: ["walrus_value", "banana_value", "cone_value"].
  • Since we know "morse" is a key in the array and corresponds to the localized key "walrus", we should display the localized text corresponding to the key "walrus_value".

Code snippet (full source-code on GitHub):

struct ChallengeView: View {
    /// The message to be displayed in the alert when a special hint is triggered.
    @State private var alertMessage: LocalizedStringResource = ""
    
    /// A flag to control the visibility of the alert.
    @State private var showAlert = false
    
    /// The word entered by the user to check against accepted keys.
    @State private var enteredWord = ""
    
    /// An array of accepted localized keys that can trigger special hints.
    private let acceptedLocalizedKeys = ["walrus", "cone", "banana"]
    
    var body: some View {
        contentView
            .onChange(of: enteredWord) { _, newValue in
                checkWord(newValue) // Check the entered word for special hints.
            }
            .alert(...) // Display the alert if showAlert is true.
    }
    
    /// Checks the entered text against the accepted keys and updates the alert message if a match is found.
    /// - Parameter enteredText: The text entered by the user to check for special hints.
    private func checkWord(_ enteredText: String) {
        let localizedKeyValueWords = Dictionary(
            uniqueKeysWithValues: acceptedLocalizedKeys.map { key in
                let translatedCopy = String.LocalizationValue(key)
                return (key, String(localized: translatedCopy))
            }
        )
        
        // Invoke the use case to find a matching special hint.
        if let specialHint = FindWordInListCaseInsensitiveUseCase.invoke(
            input: enteredText,
            wordsDictionary: localizedKeyValueWords
        ) {
            // Update the alert message with the localized key for the special hint.
            alertMessage = LocalizedStringResource(
                stringLiteral: "\(specialHint.localizedKey)\(Self.suffixForSpecialHint)"
            )
            showAlert = true // Set the flag to show the alert.
        }
    }
}

// Enum to encapsulate the logic for finding a word in a case-insensitive manner.
enum FindWordInListCaseInsensitiveUseCase {
    /// Invokes the search for a matching word in the provided dictionary.
    /// - Parameters:
    ///   - input: The user input to compare against the dictionary.
    ///   - wordsDictionary: A dictionary of words to search through.
    /// - Returns: A tuple containing the localized key and the word if a match is found; otherwise, nil.
    static func invoke(
        input: String,
        wordsDictionary: [String: String]
    ) -> (localizedKey: String, word: String)? {
        // Search for a matching word in the dictionary using case-insensitive comparison.
        if let (localizedKey, word) = wordsDictionary.first(where: { key, word in
            input.compare(
                word,
                options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive]
            ) == .orderedSame
        }) {
            return (localizedKey, word) // Return the found key and word.
        }
        return nil // Return nil if no match is found.
    }
}

Conclusion

In conclusion, localizing strings in iOS applications is a crucial aspect of creating a user-friendly experience for a global audience. Throughout this article, we explored various methods for localization, including LocalizedStringResource, String(localized:), LocalizedStringKey, and NSLocalizedString. Each method has its own use cases and advantages, allowing developers to choose the most appropriate approach based on their specific needs.

Understanding the differences between these localization techniques is essential for effective app development. By leveraging the right tools, developers can ensure that their applications not only reach a wider audience but also resonate with users in their preferred languages.

As you continue to develop your iOS applications, remember the importance of localization and consider implementing these strategies to enhance accessibility and user satisfaction. Embracing localization will ultimately lead to a more inclusive and successful app experience for users around the world.

Bonus Questions

  1. What improvements can we make to the algorithm for the challenge?
  2. What does the NS in NSLocalizedString stand for?

Resources

Here are some helpful resources for further reading on localization in iOS: