Menu

Localization

Localizing your app into multiple languages gives it the maximum possible reach. Localization is a critical part of making your app accessible to users all around the world. Skip helps unlock the promise of true universality for your app by bringing SwiftUI to Android, but the other half of the equation is ensuring that your users can understand the content of your app.

Skip embraces the xcstrings catalog format, new in Xcode 15, to provide a simple and easy solution to adding support for multiple languages to your app.

Localization example

Consider the following SwiftUI snippet from the default “Hello” app of a screen in a tab bar with a “Hello Skipper!” message.

VStack {
    Text("Hello \(name)!")
    Image(systemName: "heart.fill")
        .foregroundStyle(.red)
}
.font(.largeTitle)
.tabItem { Label("Welcome", systemImage: "heart.fill") }

For users with their device language set to French, we want the tab item to be “Bienvenue” and the message to be displayed as “Bonjour Skipper!”. This can be accomplished by editing the Localizable.xcstrings file in Xcode and filling in the translations for each supported language. String interpolation is handled by substituting the variable (e.g., "\(name)") with the token "%@", which will cause the translated string to insert any variables that need to be substituted at runtime.

To localize strings used outside of SwiftUI, use iOS’s standard NSLocalizedString function, which also requires you to insert tokens for any variables:

let localizedTitle = NSLocalizedString("License key for %@")
sendLicenseKey(key, to: user, title: String(format: localizedTitle, user.fullName))

Xcode 15 has native support for editing string catalogs with a convenient user interface:

Xcode screenshot of editing an .xcstrings file

The result is that by updating this single file, you can localize your app into many languages, enabling native speakers of those languages to use your app with ease. For example, the default “Hello” app has localizations for English, French, Spanish, Japanese, and Chinese, shown here for both the iOS and Android versions of the app:

Localization screenshot Localization screenshot Localization screenshot Localization screenshot Localization screenshot
Localization screenshot Localization screenshot Localization screenshot Localization screenshot Localization screenshot

The xcstrings Format

The Skip transpiler handles the .xcstrings localization format, which is used by Xcode 15 as a single source of truth for the app’s localization. The default project created by skip init --appid=… hello-skip HelloSkip will create a Sources/HelloSkip/Resources/Localizable.xcstrings file which can be used as a starting point for adding new languages and string translations to your project.

Xcode 15 will automatically fill in any strings that it finds in common SwiftUI components, such as Text, Label, and Button. This happens in the background, as part of a “Sync Localizations” operation that periodically scans your code for new and updated strings. You will rarely, if ever, need to manually add a localization key to the Localizable.xcstrings catalog.

The Localizable.xcstrings file is a simple JSON file which can be either edited through Xcode or handed off to specialist translators and then re-integrated back into your application. The structure is simple, and can be edited either by hand or using machine translation tools. An excerpt of the format is as follows:

{
  "sourceLanguage" : "en",
  "strings" : {
    "Hello %@!" : {
      "localizations" : {
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "¡Hola %@!"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bonjour %@!"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "こんにちは、%@!"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "你好,%@!"
          }
        }
      }
    },
    "Welcome" : {
      "localizations" : {
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bienvenido"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bienvenue"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ようこそ"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "欢迎"
          }
        }
      }
    }
  },
  "version" : "1.0"
}

The following table lists the supported tokens that will be substituted at runtime:

Token Meaning
$@ String or stringified instance
%ld or %lld integer number
%lf or %llf floating point number
%.3f formatted floating point number
%1$@ maually-specified positional argument
%% literal escaped percent sign

More information on the Xcode editor for the xcstrings format can be found at https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog.

Android has a separate localization system that involved adding keys to a res/values/strings.xml file. Skip does not use this system, as it is incompatible with modularization; adding entries to this XML file will not have any effect on a SkipUI app.


Localizing modules

SwiftUI components that accept a String, such as Text("Hello \(name)!") or Button("Click Me") { doSomething() }, as well as the standard NSLocalizedString function assume that the localized string is defined in the main bundle. On the Android side, Bundle.main uses the resources defined in the primary app module, which is the module in the app that contains the entry point and top-level resources.

However, when you modularize your app by breaking it up into separate SwiftPM modules, references to these string keys will still assume that they are defined in the main bundle, rather than in the module bundle. This assumption is built in to both SwiftUI as well as the transpiled SkipUI. This means that if you create a separate library module of UI components to be shared between apps, you need to manually specify the bundle that should be referenced for the translated keys.

Each SwiftUI component that accepts a localization string will also have a constructor that accepts a Bundle parameter. The NSLocalizedString function takes an optional Bundle as well. You generally want to use the Bundle.module value as the argument, which will cause it to reference the component’s module. For example:

VStack {
    Text("Hello \(name)!", bundle: .module)
    Button {
        doSomething()
    } label: {
        Text("Click Me", bundle: .module)
    }
}

...

let localizedTitle = NSLocalizedString("License key for %@", bundle: .module)
sendLicenseKey(key, to: user, title: String(format: localizedTitle, user.fullName))

The additional bundle parameter makes your code more verbose, but has the advantage that the component can be used irrespective of whether it is defined in the top-level app package on in a separate module.

Localizing raw strings

In addition to localizing SwiftUI components, the string localization dictionary can be accessed using the Foundation function NSLocalizedString, which enables the localization of strings that are constructed outside the context of user-interface elements.

For example, to create a local variable with a localized string:

let helloWorld = NSLocalizedString("Hello World", bundle: .module, comment: "greeting string")

The comment element is required and is used to provide context to translators.

Parameterizing strings localized in this manner is a bit more complex than SwiftUI elements, since automatic string interpolation being converted into the %@ tokens does not take place:

let personName = "Skipper"
let greeting = String(format: NSLocalizedString("Hello, %@!", bundle: .module, comment: "parameterized greeting"), personName)

Limitations

Skip does not currently handle String Catalog Plural Variants (described at https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog#Add-pluralizations).