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.

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.

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:
- When communicating between processes (like the main app and extensions),
- When you want to change the locale before displaying the string,
- 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:

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.
- Users will see a text field to enter a keyword.
- To progress to the next level, they must enter the correct keyword.
- They have opted in to receive special hints based on the entered words that are related to the keyword.
- 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
- What improvements can we make to the algorithm for the challenge?
- What does the NS in NSLocalizedString stand for?
Resources
Here are some helpful resources for further reading on localization in iOS:
- Localization Value Documentation
- Localized String Resource Documentation
- Localized Words GitHub Repository
- iOS Localization: LocalizedStringResource vs LocalizedStringKey
- NSLocalizedString Documentation
- Default Value on NSLocalizedString
- Localization in iOS Apps