When you localize the text within your app, you can help make your app more accessible to a worldwide audience. Discover best practices for building your localization workflow, including how to write and format strings accurately, and learn how to prepare strings for localization in different languages using Xcode.
♪ Bass music playing ♪ ♪ Thomas Naudet: Hello, and welcome to WWDC.
I'm Thomas from the localization team at Apple, and today I'll be showing you how to streamline your localized strings.
You will see the easiest way to make your app work in many languages.
We'll start by writing UI code.
Then we'll see how to organize strings in your targets.
After that we'll let Xcode do the heavy lifting for us.
Lastly, we'll learn new techniques for advanced strings.
With all this, we'll guarantee your app is prepared for great translations.
This is super important because text is everywhere in our lives.
We use text to communicate and get information in the real world.
It's obviously all the more true on our connected devices with all the apps, notifications, and articles we read every day.
More than ever, it's crucial the text can be read, be accurate, and be accessible.
Imagine being completely lost in this street trying to find a small shop among all those signs.
It could be the same for users of your app.
Text is such a core feature -- it guides users and brings familiarity.
In that way, localizing your app is an opportunity to reach all users around the world, especially in all the markets the App Store provides.
As a developer, you are already familiar with text, since us developers enjoy naming things.
We called text "strings." Strings are everywhere: titles, buttons, user content, online content, search queries, graphics, accessibility labels, and so on.
Think about all the strings in your app as movie subtitles.
In the movie you watch, you want all subtitles to be in the right language, at the right time, with the right context, and consistent throughout the movie.
This is the exact same here.
Your strings convey meaning to your visual content and help the user navigate through the streets of your app.
Let's see now, from a developer perspective, what the localization process is.
Your user interface presents strings in views.
Those strings live in their own files.
This helps separate the concerns between your visual content -- which is generic for all languages -- and your specific translations.
In between, the Foundation framework links your user interface and all your strings.
In this talk I'll go into details about each of these components.
That looks like a lot, but we'll uncover everything step by step.
Then let's start by defining strings in your user interface.
Displaying localized strings is easy by combining the latest Apple technologies.
I encourage you to use these techniques in your app, so that you never have to think twice about localization again! Well, any string you define in UI needs to be localized.
There are two types of visible strings.
One is directly defined as you create your view.
Here, I've defined a label in SwiftUI and Storyboard.
And yes! Both are localizable.
I told you it was easy! The second type of strings is more general and can be found in model code in SwiftUI but also in AppKit or UIKit.
You might need to store a string in a variable, or return it from a function.
In all these cases, you can use NSLocalizedString.
New in iOS 15 and macOS Monterey, you can use our refined method for Swift, String(localized:).
Let's focus on the first method in SwiftUI for a moment.
Everything you present in the interface is indeed a view, whether this a text label or a button, it's all localizable by default, ready to host your future translated strings.
Since this is enabled by default, if you have mock content such as previews, make sure you opt out by using verbatim.
This will avoid unnecessary work for translators.
You can learn more techniques in this related session.
Now that we've covered the basics, let's learn how to make this string more dynamic.
I'm currently showing a localized button to place an order.
You might not know I love concerts, and I love seeing artists live with friends even more! In this app, it's not obvious how many tickets I'm ordering for all my friends.
So let's improve that button to include the number of tickets I've selected.
That was easy.
I just inserted a variable in the string like I'm used to in Swift.
Here, count will be replaced at runtime by the actual number, 3.
Now that we've seen all our options, I want to call out a common pitfall: String with (format:).
String with (format:) is great, but it's not intended to be used for localized strings.
Let's see why with an example in Arabic where the text direction and the digits can be different.
The good news is both are managed automatically when you use String(localized:).
Now, the number "three" is correctly written in Arabic in the button, according to the user's preferred digits in Settings.
String(localized:) also supports plurals, and isolation of each part of the string, meaning words are not shuffled around in bidirectional text.
Lastly, be careful not to overuse variables.
Gluing strings together is handy but could lead to translation problems.
Here the word “Order" could be written differently in some languages whether you order now or order later.
It's safer to use two separate strings.
All right. There's one major, final aspect we need to see that I secretly kept from you until now.
The strings you've defined will need to be translated by somebody other than you.
Translators don't have the full app UI in front of them while they translate string by string, and they need to stay consistent in all strings.
So you need to help them, just like you help your coworkers understand your code by adding code comments.
I insist, no matter the string, you should always define a comment.
And don't forget your Storyboard files; they have a comment field in the identity inspector.
Let me share some tips to help translators.
First, comments should explain where the string is visible.
For instance, is this a button? A label? Some VoiceOver text? Knowing if this is an action -- to order -- or a statement -- an order -- is critical.
Second, they should explain the context.
If I press Order, am I completing a transaction or sorting a list? Lastly, comments should explain variables.
When localizers see your strings, they don't see your code, so they don't see the name of your variables to get a clue, only a generic placeholder instead.
So for the last example here, what does the number before “Ordered" represent? Is this the number of past orders? The number of tickets I've just ordered? Yes! Thanks to the comment, I know it' s the total number of tickets.
Translators in lots of languages will be able to correctly translate according to the gender of the word "tickets." Sometimes the easiest is also to write an example value of the variable in the comment.
With that in mind, there it is.
You have your UI code ready for localization.
Now, we'll learn how you can maintain your UI code separately from your strings.
Basically, you'll be able to organize where your strings will live.
In order to do this, let's see how Foundation will make sure that your code loads the correct localized strings files.
Of course, localization starts with a language.
You can head into your project settings to add a new one.
Let's take a look at Xcode.
Select your project and click the plus(+) button to add a localization.
You will see the menu gained lots of new languages in the latest version.
Let's see... In our app list of localizations, it starts with Base at the top.
Indeed, this where the UI elements live because they are shared across languages.
For instance, a Storyboard file is shared; a Siri Intents file is shared too.
Then they need to be added to Base.
So make sure you click the Localize button for all shared assets.
OK but your strings, on the other hand, are not shared.
Strings belongs to one language.
So you will have your strings in English and all your strings in Arabic.
To test your app with strings in a certain language, you can change the preview environment in SwiftUI or change the app scheme settings.
If the device is in a language your app doesn't support, Foundation tries to find the best alternative.
If my phone is in Mexican Spanish, Foundation will try to fallback on Latin American Spanish, then to Spanish, then to your app development language -- for instance, English.
Lastly, when strings come from a server, make sure you respect the user's preferred language.
We've seen that each language has a collection of strings.
Those strings can be further organized into files called "tables." You can use this feature to organize every string as you'd like.
For instance, you could have a table for each feature, or each screen.
Since this is optional by default, all strings are put in a table named Localizable.
Concretely, this means all strings are stored in a file named Localizable.strings.
Let's look at an example to recap what we've learned so far.
I have the following code declaring a string with a variable, a customized table name, and a comment.
Let's say my app supports French.
In Xcode, you will then need a UserProfile.strings file containing the French translation from English.
It will be stored in a French resource folder named by convention, fr.lproj.
The comment was very useful for the French translator.
They used an "e" to mark the feminine, as "places;" French for tickets is feminine.
Let's spice things up a little bit and talk about bundle.
This parameter allows loading strings across targets.
The bundle parameter is main by default.
In your own app, you don't need it.
Main is that app.
In your app extension, main refers to your own extension, so you don't need it either.
But let's say you want to share a string between your app and its extension.
In your extension, you will need to provide the bundle of the main app.
Doing so avoids duplicating the strings between the two.
You could also get your strings from a framework.
In that case, either you tap directly into the framework's strings by specifying its bundle in your app code, or the framework vends you variables you can directly use.
Those localized string variables have been defined in the framework by specifying the framework's own bundle.
Let's look back at our model, only now we'll update it to make our app load the string provided by the framework.
The framework defines the string and informs Foundation the translation is stored inside its own bundle in a strings file.
Without this parameter, the string would be fetched from the hosting app instead and not found.
In practice, it looks like this.
You supply the bundle where your text is stored.
The framework will look for the strings into its own bundle.
Then it's easy for your app.
The string can be used in a simple line of code.
Moreover, if you implement localization in your frameworks this way, you won't even have to create the strings file presented here containing “Complete.” Let's now see why.
We've seen everything you can do in code to declare and organize strings.
But we haven't seen how to actually create the files that will hold your translations.
It turns out you actually don't need to create those strings files.
Xcode can take care of creating all the .strings files for you.
When you use Export Localizations it reads your code and extracts all your strings.
This is great because you don't have to maintain strings files.
If you used to forget to localize the string you've just coded in your UI, this is for you.
If you struggle with ‘genstrings,’ this is for you too.
This year, in Xcode 13, we've added compiler support for Swift strings extraction.
Also, workspaces are now fully supported.
This further separates the concerns between your logic and your translations.
Xcode will detect and extract text from the Swift and Foundation methods we've seen earlier.
Please be aware, if you have custom code that wraps those APIs, this will not work by default.
You shouldn't have to use a method or macro usually, but if you really need to, you can add them to your build settings under Localized String Macro Names.
For the rest, Xcode will extract for localization your app name and privacy descriptions defined in Info.plist, and in general all assets marked as Localized in Xcode's inspector.
If you have existing localizations, you can already transition to Xcode's export for your new UIs.
The new strings will be automatically added to your existing files.
This is great if you want to convert your project at your own pace.
As a bonus, screenshots from your UI tests are now included.
It's great for your localizers so that they get context where a string is, and it's awesome for you to showcase localized screenshots of your app on the App Store.
All right, Xcode extracted all the localization catalogs, so you'd think it's the translators' turn to work now.
Actually, you can do it too! New in Xcode 13, exported localization catalogs can be viewed and edited directly in Xcode.
You can see each generated file -- so each table -- in their bundles on the left.
And for your selection, you can see all the contained strings, images, and files.
You can filter strings, sort them, see comments, screenshots, and even translate! This is very convenient if you develop and translate your own app.
It lets you review and bugfix strings yourself, too.
When translators send you back the translated string catalogs, you can import them into your project using the Product menu in Xcode.
And boom! Your strings files, stringsdicts, and other assets will be created and updated.
Using the command line equivalents, you can run an automated export and import on a continuous integration system.
By calling them regularly, your project gets the most up-to-date strings, and a fast turnaround at getting your new UI translated.
You can see the enhanced workflow in action in this year's session, "Localize your SwiftUI app" and learn more in the introductry session.
That's it! You've witnessed how strings are born and live in your app.
To conclude, I'll show you how to deal with some more complex strings.
And I'm sure you'll love the new features we have in store.
Let's start with a great one! We've improved attributed strings to have built-in localization.
It's now possible with the support of the Markdown syntax! You can now localize your strings without losing any formatting.
No more risky character operations, just to make one word in bold.
Speaking of which, here I use asterisks to put a strong emphasis on “complete.” I encourage you to check "What's new in Foundation" to learn how you can add a link, an emphasis, monospace text, and so on.
We've seen that one string you define in code will have one translation in a strings file.
But sometimes you need your string to have multiple representations.
This is possible with a stringsdict file, a collection of strings adapting to rules you define.
For instance, remember our example where we would like to order several tickets? In English, we would add an "s" as a suffix if there are multiple tickets and no "s" for one ticket.
To keep your code simple and correct, you need to define this plural rule using a stringsdict.
Because if you localize your app, those rules will be different for each language.
Look at some of the cases in Russian.
You don't want to deal with that in code, it's rather something localization should take care of.
Let's see how we would implement that.
No code change needed; we will still use our existing code above.
First, compared to strings files being made for you, stringsdict is a manual opt-in.
So create one using the Xcode template, and make sure you clicked Localize in the inspector.
All right, it all starts with the string you've defined in code.
If you have multiple plural strings in code, you can add this root entry here for each of them.
Inside, you define the actual value presented.
This value follows a search-replace mechanism.
Here in this example I've defined one token named "tickets." It will hold the fulll final string.
It's best to include most of the text inside tokens, but that field will be localizable in case translators need to add a prefix, a suffix, or move the tokens around in case you have multiple variables.
OK, that token will vary according to your one variable in code.
Then let's define that tickets token.
First, we just confirm that we're doing some pluralization.
Then we indicate the variable is a number with the C-style formatter d.
And finally, we can write the plural rules.
In English, we can just declare the rule "one" and "other" and "zero", if you'd like.
For each entry, we write the actual value of the tickets token.
Then, if we have three tickets, in English it will use "other" and generate “Order 3 Tickets".
by replacing %d with the number 3.
If a language needs more cases, don't worry.
Xcode will add them for you at export time.
In Russian it will add “few” and “many” on top of the existing ones.
That's it! Our string will be pluralized at runtime.
Before we move on, I want to call out one case.
Although stringsdict should be used for plurals, it's meant for strings containing a number.
We've seen earlier the case "one" for singular in English.
It is indeed for the number one in Russian as well, but, it's also used for 21, 31, and so on.
Then in that case, using stringsdict would not be correct as you just want equals to one only.
In this example, the plural varies on this, both, or all.
There is no number; please don't use a stringsdict.
Imagine me booking 21 tickets for all my friends, and I would see in Russian, "Order This Ticket" instead of "Order All Tickets." I would ask you, "Where's my money?" Instead use this simple but effective if/else for correct pluralization of each of the three strings, in all languages.
And that was plural support, but stringsdict can handle many more variant types of a string.
I invite you to watch the related sessions to learn more.
This is great, but we wanted to provide you a simpler way.
Yes, Foundation learned this year how to do grammar for you! With Markdown support added to attributed strings, use this new format with the inflect attribute, and you'll get the right value computed at runtime, like on this button.
This is a great new addition to iOS 15 and macOS Monterey, currently available in select languages.
If you want more control, you can use a stringsdict.
We didn't stop at adding plural support.
We wanted our software strings to be more inclusive.
For instance, when an app welcomes users, it's pretty straightforward in English.
But it is not in Spanish because it depends on the user's term of address.
Up to now, you had to present a nonpersonalized string, which is correct but stilted to most Spanish users.
Literally, "We give you our welcome." So what if we could personalize the string for the user? With our new Markdown notation, you can do that now! The string will now follow the term of address you select in the language settings of your device.
So “Bienvenida" for users who want to be referred to in the feminine, “Bienvenido" in the masculine, and if we do not know, we'll use the existing inflection alternative.
Inline inflections for plural and term of address can be defined either in your code or your translations.
We are super excited about this new addition to select languages, and we can't wait to see your app use it.
Finally, we've seen today all the ways you can write localizable strings.
But if you want to present data, you should actually let the frameworks write them for you.
Our formatters handle hundreds of combinations of languages and regions, and a variety of types and units.
So don't hardcode; let us do the hard work.
And this is easier than ever starting this year to adopt formatters in Swift.
We made them easy to use inline in your string interpolation.
Check this year's "What's new in Foundation" to find your new favorite API and get details about all those we've seen today.
If you need to deploy to previous releases or want details on formatters, check last year's session.
All right, what I want you to take away today is that if you write code using modern APIs, Xcode will generate all the strings for you.
We saw how you can organize your strings across bundles, and we discovered new APIs to make grammar and formatting easy for you.
If you follow all these techniques, adding a new language won't require any code changes! Lastly, always test.
No matter how much effort you put into localizing your strings, you should test your app to make sure it runs great in all languages.
With that, I can't wait to book concerts with your fully localized app.
["pop", "rock", "electronic"].formatted(.list(type: .or)) // pop, rock, or electronicText("Total: \(price, format: .currency(code: "USD"))", // Total: $9.41
comment: "Order subtitle: total price of all tickets")
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.