Localization

Overview

There many facets to localization including text translation, enabling users to select a locale within an app, plural forms, numbers, currency, dates, and images. Each of these are discussed here.

Many SwiftUI views automatically perform translations. These include Text, Label, and Button.

Resources

Specifying Supported Languages

  1. Select the top entry in the Project Navigator.
  2. In the left nav of the project editor, select the project.
  3. Select the "Info" tab.
  4. In the "Localization" section there will already be an entry for "English". For each additional language to be supported, click the "+" Button and select a language.

String Catalogs

Support for String Catalogs was added in iOS 17. These supercede "Strings" and "StringsDict" files. Behind the scenes the build process compiles String Catalog files to Strings and StringsDict files, but those do not appear in the Project Navigator.

Existing String and StringsDict files can be converted to String Catalog files. To do so, right-click on any one of them and select "Migrate to String Catalog...". In the dialog that appears, select all the migratable files that should be migrated. Then click the "Migrate" button.

A String Catalog is a JSON file that stores the translations for all supported languages. Most projects will have a single String Catalog file.

Xcode provides a specialized editor for String Catalog files that displays a separate table for each supported language.

Creating

To create a String Catalog file:

A project can use multiple String Catalog, but for most projects there is no reason to have more than one.

Languages

There are two ways to add a language to a project.

The older, long way is to:

The newer, short way is to:

Either way all current translation keys will now appear in the String Catalog editor for the new language, ready for their translations to be entered.

To delete a language:

Languages cannot be deleted from the String Catalog editor.

Automatic Keys

A String Catalog can be automatically populated from string literals in source files that define SwiftUI views. To enable this:

Xcode Build Settings for Localization

Once this is enabled, the String Catalog file will be updated during each build.

The build process looks for string literals in view code. It uses all strings passed to functions where the parameter type is LocalizedStringKey.

It also looks for the following kinds of values in non-view code. Use these instead of plain String values to indicate that they require language translation.

LocalizedStringResource(
"some text",
table: "SomeTable", // optional
local: someLocale, // optional
comment: "some comment" // optiona
)

String(localized: "some text")

AtributedString(localized: "some text")

LocalizedStringResource is a new type that can hold the following four components of localizable strings:

For long sentences or paragraphs, it is useful to choose a key rather than typing the full text into a source file. This requires entering the translation for the default locale (often English).

For example, the key "app-overview" could have the English translation "This app provides weather forecasts in a delightful way."

Translations

To add or edit translations for a given language:

For languages that are missing translations, a percentage of supplied translations is displayed after the language name. Once all the translations for a language are supplied, the percentage is replaced by a green check mark. When all languages have a green check mark, language translation is complete for the current set of keys.

The last column in the translations tables indicates the state of the translations which can be one of the following:

Keys for stale translations can be deleted if it is suspected that it will never be used in the future.

After a translation that needs review is reviewed, right-click it and select "Mark as Reviewed".

To delete a translation, select it and press the delete key. This removes the key from all languages, not just from the currently selected language. Do not press cmd-delete because that offers to delete the entire String Catalog file!

Manual Keys

Translations can be manually added to String Catalog. This enables working on translations before their keys are actually used in the code.

To add a new key:

Manually added keys are not managed by Xcode. This means they will not be automatically updated or removed by Xcode. To cause a manually added key to be managed by Xcode, select its row, open the inspector on the right, and change the value for "Managed" from "Manually" to "Automatically".

Xcode Strings Catalog Inspector

Plurals

To support plurals in a translation:

Repeat the steps above for each language that requires platform-specific translations.

When there is more than one number placeholder in a key, each is given a name like @arg1. These placeholders can be renamed to make their meaning more clear. To rename a placeholder, click the name in its sub-row and enter a new name that still begins with @.

In the example above, @arg1 might be renamed to @dogs and @arg2 might be renamed to @squirrels.

The following view code might be used with the example translation.

VStack {
HStack {
Stepper("Dog Count", value: $dogCount)
Text("\(dogCount)")
}
HStack {
Stepper("Squirrel Count", value: $squirrelCount)
Text("\(squirrelCount)")
}
Text(
String(
localized: "The \(dogCount) dogs barked at the \(squirrelCount) squirrels."
)
)
}

The following screenshot shows the definition for this plural translation in the Strings Catalog file.

Xcode Strings Catalog plurals

The full code for this project can be found at Localization2023Demo.

As of Xcode 15 beta, translated plurals always display English in previews instead of the requested locale. However, they display correctly in the Simulator.

Device-specific Translations

The translated strings can vary by device. For example, the word "tap" which is appropriate for iOS and iPadOS can be changed to "click" for macOS.

To add device-specific translations for a key:

This will add sub-rows to the row where each row begins with a platform name or "Other".

Xcode Strings Catalog Vary by Device

Repeat the steps above for each language that requires platform-specific translations.

More Strings Catalog Editor Features

The "Filter" input can be used to filter the list of translations to those that contain given text in their key or translation.

Any table column heading can be clicked to sort the rows on that column in either ascending or descending order.

Strings Files

These were used before iOS 17. For iOS 17 and beyond, using String Catalogs is preferred.

Creating a Strings File

Translations are described in a "Strings" file. To create one:

  1. Add a file to the project by pressing cmd-n.
  2. In the Resource section, select the "Strings File" template.
  3. Click the "Next" button.
  4. Keep the default directory and the default file name "Localizable.strings".
  5. Click the "Create" button.
  6. Select the "Localizable.strings" file in the Project Navigator.
  7. Open the Inspector panel on the right.
  8. Under "Localization", click the "Localize..." button.
  9. In the dialog that appears, click the "Localize" button.
  10. Once again, select the "Localizable.strings" file in the Project Navigator.
  11. In the Inspector panel on the right under "Localization", check all the language checkboxes. This will add one entry under "Localizable.strings" in the Project Navigator for each selected language.

Populating a Strings File

  1. In the Project Navigator, expand the "Localizable.strings" entry to expose an entry for each supported language.

  2. For each supported language

    1. Select its entry in the Project Navigator.

    2. For each string to be translated

      1. Enter an assignment statement of the form "key" = "translation";

        Note the semicolon at the end.

For example, the key could be "greeting", the English translation could be "Hello", and the French translation could be "Bonjour".

When no translation is found for a given key in the current language, the key itself is used.

If a key is the same as its English translation then it is not necessary to add an entry for it in the English file. For example, if the key is "Hello" then there is no need to add "Hello" = "Hello";" in the English file and the French file could contain "Hello" = "Bonjour";. This is the recommend approach unless the English translation is long.

Translation strings can contain Markdown syntax. For example, this can be used to make some of the text bold, italic, or underlined.

Comments using the syntax // or /* ... */ can be used.

To validate a Localizable.strings file, cd to the project directory that contains the .lproj files and enter plutil -lint {language-code}.lproj/Localizable.strings.

Preview Locale

To select a locale to use in the Preview, apply the environment view modifier to the top-most view. For example:

ContentView()
.environment(\.locale, .init(identifier: "la"))

where la is replaced by a language abbreviation such as en for English or a combination of a language and region abbreviation such as en-US.

A view can have multiple previews. A button for each preview is displays at the top of the canvas. Click a button to see the corresponding preview.

Xcode Preview buttons

#Preview("English") {
ContentView()
.environment(\.locale, .init(identifier: "en"))
}

#Preview("Spanish") {
ContentView()
.environment(\.locale, .init(identifier: "es"))
}

#Preview("French") {
ContentView()
.environment(\.locale, .init(identifier: "fr"))
}

Simulator and Device Locale

To select a locale in the Simulator or on a device:

Another way to select the locale to use is to:

User-selected Locale

The environment view modifier can be used to allow the user to select a locale within the app.

This Apple Forums post says the following:

If the system puts up a UI component on your behalf it will do so in the localization that it thinks that your app is running in. If your app is 'pretending' to run in some other language, you'll end up with a mixed localization, where some parts of your app (the parts you control) are in one language and other parts of your app (the parts controlled by the OS) are in another. That's a very poor user experience.

Despite this warning, the environment view modifier can be used to change the current locale. For example:

        VStack {
Picker("Locale", selection: $language) {
Text("English in U.S.").tag("en-US")
Text("French in France").tag("fr-FR")
Text("Spanish in Spain").tag("es-ES")
}
.pickerStyle(.segmented)

Text("Hello!")
}
.padding()
.environment(\.locale, .init(identifier: language))

Changing the locale in this way rather than in the Settings app works for many kinds of translations, but not all.

Either of the following approaches can be used to get a translation in code:

let translation = Bundle.main.localizedString(
forKey: key,
value: key, // translation defaults to the key
table: nil // defaults to "Localizable"
// which uses the file Localizable.strings
)

// This is a shortcut for the above where table is nil
// which causes it to use `Localizable` for the table.
let translation = NSLocalizedString(key, comment: "")

These approaches get a translation based on the locale selected in the Settings app. However, neither of these honor the locale specified with the environment view modifier.

The same issue exists with retrieving localized images from an "Image Set". TODO: Is this a bug in SwiftUI?

Localizing Strings

The macro returns a localized string. To simplify use of this, define the following String extension:

import Foundation

extension String {
var localized: String {
return NSLocalizedString(self, comment: "")
}
}

To use this, add references to this new computed property to any string. For example:

print("Hello".localized)

let greeting = "Hello"
print(greeting.localized)

Localized String Arguments

Localized strings can specify arguments accepted and where they should be inserted in translations. Arguments are represented by %@ for String values, %lld for Int values, and %f for Double values. Arguments are passed to the LocalizedStringKey initializer using string interpolation.

For example, the following translations can be defined:

// English
"Annie %@ %@" = "The %@ will come out %@.";

// French
"Annie %@ %@" = "Le %@ sortira %@.";
"sun" = "soleil";
"tomorrow" = "demain";

// Spanish
"Annie %@ %@" = "El %@ saldra %@.";
"sun" = "sol";
"tomorrow" = "mañana";

The following code uses these translations:

let thing = "sun"
let time = "tomorrow"
Text("Annie \(sun) \(tomorrow)")

TODO: How can we translate the strings "sun" and "tomorrow"?

Stringsdict Files

Stringsdict files provide a way to define pluralization rules for translations. These were used before iOS 17. For iOS 17 and beyond, using String Catalogs is preferred.

SwiftUI can automate displaying phrases that describe a number of things where the word that describes the thing varies based on the count. For example, "dog" vs. "dogs" or "cactus" vs. "cacti".

There are two kinds of plural values to consider. Cardinal numbers specify a count or quantity (ex. two). Ordinal numbers specify a position within an ordered list (ex. second).

Pluralization of cardinal numbers is supported by a .stringsdict file which is described in this section. Pluralization of ordinal numbers is supported by NumberFormatter as described in the Numbers and Currency section.

To define how specific words should be pluralized based on a count:

  1. Add a file to the project by pressing cmd-n.
  2. In the Resource section, select the "Stringsdict File" template.
  3. Click the "Next" button.
  4. Keep the default directory and the default file name "Localizable.stringsdict".
  5. Click the "Create" button.
  6. To support multiple languages:
    1. Select the "Localizable.stringsdict" file in the Project Navigator.
    2. Open the Inspector panel on the right.
    3. Under "Localization", click the "Localize..." button.
    4. In the dialog that appears, click the "Localize" button.
    5. Once again, select the "Localizable.stringsdict" file in the Project Navigator.
    6. In the Inspector panel on the right under "Localization", check all the language checkboxes. This will add one entry under "Localizable.stringsdict" in the Project Navigator for each selected language.
  7. For each supported language:
    1. Select its entry in the Project Navigator.
    2. Add rows with keys, types, and values similar to what is shown in the screenshot below for each phrase to be pluralized. A row and all its descendant rows can be copied from one language entry to another to save typing.
SwiftUI English plural localization
English plural rules
SwiftUI French plural localization
French plural rules
SwiftUI Spanish plural localization
Spanish plural rules

The supported format specifiers include:

%lld works for Int values, but it seems the %d and %u do not!

The following keys are used to define how counts should be pluralized. Their meaning differs based on the locale, especially the meaning of "few" and "many".

KeyEnglish Meaning
zero0
one1
two2
few3
many> 3
otherany value not specified

Many languages such as English, French, German, Italian, and Spanish only use zero, one, and other. Chinese and Japanese only use one form. Arabic uses all six forms.

For more details see the Unicode Common Locale Data Repository (CLDR) Plural Rules and Language Plural Rules.

The following code demonstrates displaying a number of apples. The string to be formatted must contain one string interpolation. The count can be hard-coded in the interpolation or provided by a variable.

Text("apple \(2)") // a pair of apples
let appleCount = 7
Text("apple \(appleCount)") // 7 apples

Numbers and Currency

The NumberFormatter class will localize date formats when its locale property is set.

Currency formatting requires the locale identifier to contain both a language code and a country code.

The following code demonstrates all the supported number formatting styles:

let number = NSNumber(value: 1234.5678)
let formatter = NumberFormatter()

func demo(_ style: NumberFormatter.Style) {
formatter.numberStyle = style
formatter.locale = Locale(identifier: "en-US") // English U.S.
print(formatter.string(from: number) ?? "invalid")
formatter.locale = Locale(identifier: "fr-FR") // French France
print(formatter.string(from: number) ?? "invalid")
}

demo(.none)
// 1235
// 1235

demo(.decimal)
// 1,234.568
// 1 234,568

demo(.percent)
// 123,457%
// 123 457 %

demo(.scientific)
// 1.2345678E3
// 1,2345678E3

demo(.spellOut)
// one thousand two hundred thirty-four point five six seven eight
// mille deux cent trente-quatre virgule cinq six sept huit

demo(.ordinal)
// 1,235th
// 1 235e

demo(.currency)
// $1,234.57
// 1 234,57 €

// From the Apple documentation,
// "This style behaves like the .currency style,
// except that negative numbers representations are
// surrounded by parentheses rather than preceded by a negative symbol.
demo(.currencyAccounting)
// $1,234.57
// 1 234,57 €

demo(.currencyPlural)
// 1,234.57 US dollars
// 1 234,57 euros

Dates

The DateFormatter class will localize date formats when its locale property is set. This works with predefined date/time styles and also with custom date/time templates. For example:

let date = Date()
let formatter = DateFormatter()

formatter.dateStyle = .short
formatter.timeStyle = .short
formatter.locale = Locale(identifier: "en") // English
print(formatter.string(from: date)) // 1/3/23, 1:36 PM

formatter.locale = Locale(identifier: "fr") // French
print(formatter.string(from: date)) // 3/01/2023 13:36

Text("apple \(0)")
Text("apple \(1)")
Text("apple \(2)")
Text("apple \(3)")
Text("apple \(4)")
let appleCount = 7
Text("apple \(appleCount)")

formatter.setLocalizedDateFormatFromTemplate("dd MMMM")
formatter.locale = Locale(identifier: "en") // English
print(formatter.string(from: date)) // 03 January

formatter.locale = Locale(identifier: "fr") // French
print(formatter.string(from: date)) // 03 janvier

For a summary of patterns for formatting dates and times see How To Format Date Time In Swift.

Images

To render a localized image where a different image is displayed for each supported language:

  1. Add an "Image Set" to the Assets.xcassets file.
  2. Open the Inspector pane on the right side.
  3. Click the last tab which shows the "Attributes Inspector".
  4. In the "Localizations" section, check all the languages to be supported. This opens a separate area in the image editor panel for each language where 1x, 2x, and 3x images can be added.
  5. Add at least one image size for each language.
  6. Render the image by passing the image set name to the Image view in the same way as for any other image.

The following code assumes that an Image Set named "landmark" was created in the Assets.xcassets file and it contains different images for English, French, and Spanish.

Image("landmark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 150)
.id(locale) // This does not help!

The image changes when the user changes the region specified in the Settings app. For the steps to do this, see ## Simulator and Device Locale.

If the locale is changed within the app using the environment view modifier, localized images do not update. Perhaps this is a SwiftUI bug.

Another approach that does work is to create a separate Image Set for each supported locale. The following code assumes that Image Sets named "landmark-en-US", "landmark-fr-FR", and "landmark-es-ES" were created in the Assets.xcassets file. It does update when the value of the locale variable changes.

Image("landmark-" + locale)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 150)

Device Testing

To change the default language on an iOS device:

  1. Open the Settings app.
  2. Tap "General".
  3. Tap "Language & Region".
  4. To add a language to the list of "PREFERRED LANGUAGES":
    1. Tap "Add Language..."
    2. Tap a language in the list.
    3. Tap a button to either use that language now or continue using the current language.
  5. To change the current language, drag it to the top of the list using the drag handles on the trailing edge of the list rows.
  6. To delete a language, swipe its row to the left, tap the "Delete" button, and tap the "Continue" button.

It can be important to also select a specific region. For example, an iOS device in the United States will typically have the language set to "English" and the region set to "United States". If the language is changed to "Français", but the region remains set to "United States" then dates will not be formatted in the expected way for French. Changing the region to "France" corrects this.

To also change the default region on an iOS device:

  1. Tap "Region"
  2. Tap the name of a region.
  3. Tap the "Change to {region-name}" button.

Exporting and Importing Localizations

Localization strings found in String Catalog files, Strings files, and StringsDict files can be exported to a file that can be sent to localization experts that will supply translations. Once the file is returned, it can be imported into the project to update the files that define them.

When using String Catalogs:

Xcode Build Settings for Localization

To export the localizations:

The new directory will contain one localization catalog (.xcloc directory) for each supported language. A localization catalog contains several subdirectories and files. The translations for a localization catalog can be found in the .xliff file inside the "Localized Contents" subdirectory.

XLIFF is an industry standard XML format. XLIFF files contains one trans-unit element for each translation. These elements contain the child elements source, target, and comment. People with language translation experience can edit these files, modifying the target and comment element contents.

To import modified localizations: