Innovaptor Logo

Update to our iOS Localization Workflow

More than three years ago, I wrote about a localization workflow that works well in practice. It was based on the genstrings tool to scan the source code for usages of NSLocalizedString and update the .strings files accordingly. A custom script as part of the build process which would update all .strings files, keeping existing translations around (vanilla genstrings would just overwrite those).

The workflow as like this:

  • Write code, use NSLocalizedString (with key, default value, comment, and sometimes custom localization table) for user facing strings
  • Run the update_localization script as a build step to keep .strings files up to date for the reference language via default values
  • Upload reference language .strings to translation tool
  • Translate
  • Export translated .strings files from translation tool into Xcode

This workflow worked well for years. I liked the fact that the truth was in the code and unused entires were automatically cleared from the .strings file.

Swift

Unfortunately, this is not possible anymore in swift because the genstrings tool still only works for the most basic use of NSLocalizedString in Swift with only key and comment parameters. As soon as additional parameters such as tableName, value, or bundle are used, it breaks down (rdar://22817000).

New Workflow

Our new workflow is based on R.swift. R.swift parses your Xcode project and generates a fully typed, compile time checked struct called R via which you can access your resources.

Now the workflow is reversed:

  • Write manual entries in the .strings file
  • Run R.swift (typically as a a run script phase during builds)
  • Access localizable strings via R.string

Example

As a quick example, say we want to localize the title view. First, we’re adding an entry to Localizable.strings:

  /*This is the title of the profile view*/
  "view.profile.title" = "Profile";

After running R.swift, the R.generated.swift file now contains the following

/// This `R` struct is code generated, and contains references to static resources.
struct R: Rswift.Validatable {
  ...
   /// This `R.string` struct is generated, and contains static references to 1 localization tables.
  struct string {
    /// This `R.string.localizable` struct is generated, and contains static references to 389 localization keys.
    struct localizable {
      ...
      /// Base translation: Profile
      /// 
      /// Locales: Base, de
      static func viewProfileTitle(_: Void) -> String {
        return NSLocalizedString("view.profile.title", comment: "")
      }
      ...
    }
  ...
  }
  ...
}

In code, we can access this now via the R struct:

  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    self.title = R.string.localizable.viewProfileTitle()
  }

Note that the strings key has been converted from view.profile.title to viewProfileTitle. Whats great about this, is that thanks to the generated struct, the strings are checked by the compiler and even autocompletion works!

Sometimes, when there’s only one localization table anyway, I like to use the following typealias for shorter access to the strings:

/// Typealias for shorter access to localized strings
private typealias S = R.string.localizable

which finally leads to this code:

  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    self.title = S.viewProfileTitle()
  }

Final Thoughts

Our localization Workflow on iOS is now back at what I presume to be the “default” approach anyway, however with the additional help of R.swift its now a lot more convenient than before.

Actually, the workflow is now the same again as on Android, where the approach of accessing resources via the R class was available right from the beginning. To be honest, I’ve always been a little bit jealous about its convenience 😇

Oh an by the way, I’m pretty sure it works just a well if you prefer SwiftGen over R.swift.

Markus Chmelar Portrait

Markus Chmelar, MSc

Markus is a technical mastermind and one of the founders of Innovaptor. He studied Computer Engineering at Vienna University of Technology and contributes a valuable diversification to Innovaptor's qualifications. Markus is eager to find innovative solutions to complex problems in order to provide products that are tailored directly to the customers' needs