By and large, the basis of Foundation’s localization support dates back at least to the earliest days Mac OS X 10.0. Prior to the localization workflow enhancements introduced in the Xcode 10 beta, the last major change to localized development felt like the addition of Base Localization in Xcode 6.

That’s not to say to state of localization has been unchanged over the intervening years. In fact, if your experience with localization is rooted in the days of manually editing Localizable.strings files, you might find yourself at a loss trying to adapt to the modern localization process.

Issues with Localizable.strings

Localizable.strings files have long housed the mappings of our string identifiers (frequently the native language representation of our strings) to display strings. While serving their purpose with aplomb, the venerable file format is not without issues.

Localizable.strings example

Example of a localized .strings file.

For starters, while the .strings file format is the standard for Apple-oriented development, it’s not an industry standard. If you’re working with outside localizers as part of your development process, you either need to find ones that understand the file format or translate your files into a format they do understand.

Additionally, the standard tooling around .strings files is somewhat limited. Tools like genstrings support creating an initial .strings file from your code, but updating the files during development is largely a manual process.

This manual process can also be a bit error prone. A mismatch between the stringly-based identifiers in your .strings file and your code could lead to missing localizations for some users. Older versions of Xcode treated your .strings files like resources and simply copied them into your application — a missing “;” in one of your files could completely mess up your interface for the affected translation. More recent version of Xcode convert .strings files to binary plists, so the compiler will let you know about malformed files, but the error messages still don’t point you towards the specific errors.

stringsdict Files

Another area where the .strings format fell short was support for pluralization. At one point or another, many of us have probably written code that looks like this:

let localizedString: String
if count == 0 {
    localizedString = NSLocalizedString(
        comment: "No items")
} else if count == 1 {
    localizedString = NSLocalizedString(
        comment: "One Item")
} else {
    let format = NSLocalizedFormat(
        comment: "Some Items")
    localizedString = String.localizedStringWithFormat(format, count)

The result is perfectly valid… if your app is running in English. However, pluralization rules can vary from language to language. Whereas English has rules for one and other cardinalities, a language like Irish has different rules for one, two, few, many, and others. Handling these in code would require you to learn the rules for every supported language and would likely reduce your pluralization display code to utter spaghetti.

Apple addressed this issue with the introduction of the stringsdict file format in Xcode 5. The stringsdict format replaces the one-to-one identifier-display string mapping with dictionaries containing more expressive rules for each identifier.

Pluralized stringsdict entry for an English localization.

Defining pluralized string variants for the English localization.

Since the pluralization rules now map to a single identifier, the above code simplifies to:

let format = NSLocalizedString(
    comment: "Some Items")
localizedString = String.localizedStringWithFormat(format, count)

In iOS 9 and OS X 10.11, apps also gained the ability to leverage display-context specific strings from stringsdict files. This feature lets us associate display variants with a single identifier with the available window size.

Adaptive width stringsdict entry.

Defining variable width string variants for the English localization.

The simplest way leverage adaptive width strings is just to treat them like any other localized string:

let adaptiveString = NSLocalizedString(
    comment: "Variable width testing string"

In this case, UIKit will pick the appropriate representation based on the number of em units fitting in your app’s window.

If, for some reason, you don’t want the default behavior, you can request specific lengths using NSString.variantFittingPresentationWidth(_:):

	let longestString = (adaptiveString as NSString).variantFittingPresentationWidth(50)

As Daniel Martín points out in his excellent post on the topic, the string returned by this method will be subject to UIKit’s em width heuristics. As far as I can tell, the suggested solution of using string interpolation no longer works as of Xcode 10. I’ve yet to find a suitable replacement, so that’s something to keep in mind if you’re intending to use your custom sizing logic.

XLIFF Support

While the introduction of stringsdict addressed some of the language-oriented shortcomings of the .strings files, it did nothing to address the inherent fragility of the format. Progress in that area didn’t come until Xcode 6 added support for the XLIFF format. Unlike .strings, XLIFF is an industry standard. Further differing from .strings, XLIFF files aren’t bundled in your app. XLIFF’s an interchange format — Xcode can generate the files when it’s time to request your translations, then ingest the updated files you get back from your translator.

What makes XLIFF support so interesting to me as a developer is how the files are generated. While Xcode will happily process your base language’s .strings file, it can also directly scan your project for NSLocalizedString calls and generate the XLIFF files straight from code. Once you cross that bridge, you no longer have to worry about things like mismatched string identifiers or tediously updating your .strings files because your latest updated added 12 strings and edited 3 more across 7 different languages. Since XLIFF is a variant of XML, you can also use your favorite XML processing techniques to automate any changes to the file. For instance, I will sometimes tag dummy strings with a known comment and use a XSL transformations to remove them before sending them off to my translators.

That’s not to say the XLIFF workflow was painless from the get-go. In fact, the XLIFF round-trip process didn’t gain support for stringsdict files until Xcode 9. Furthermore, that stringsdict support was initially limited to the language pluralization rules. It’s only as of Xcode 10.1 that the export and import workflow gained support for round-tripping variable-width strings. Previous version of Xcode not only ignored variable-width strings when exporting, they also failed to honor the width variants on import and would overwrite any existing width translations.1

With the addition of improved variable-width string support, the stringsdict support in the XLIFF workflow now feels complete. The generation process is aware of the language-specific pluralization rules and the exported files includes the appropriate translation requests instead of just mirroring the items in your development language’s stringsdict file… and yes, you do need to provide a stringsdict file in your base language. Unlike .strings’s 1:1 relationship between the identifier and the display string, the NSLocalizedString() call site doesn’t contain enough information to synthesize a stringsdict entry.

Translation Generation

While not maintaining your .strings files can simplify your project management, it may require a different approach to how you deal with strings in your code.

When translations are backed by a .strings file, it acts as the source of truth. If a key exists, the matching value is used, regardless of anything else in the NSLocalizedString() call site. As this output from xcodebuild shows, that’s not the case when generating XLIFF files

$ xcodebuild -exportLocalizations -project LocalizationTest.xcodeproj/ -localizationPath ~/Desktop/LocalizationTest
Key "localized_message" used with multiple values. Value "Localized String" kept. Value "Another Localized String" ignored.
Key "localized_message" used with multiple values. Value "Localized String" kept. Value "localized_message" ignored.
extractLocStrings: warning: Key "localized_message" used with multiple comments "Another Localization test comment" & "Localization test"

In this contrived example, you can see that the generator picks the first value is sees for the localized_message key and ignores the rest. Since the “first” instance may change as your codebase evolves, you need to be careful about how you define your base translations.

In light of this, I strongly recommend centralizing your reused localized strings somewhere in your app. Then, reference those values instead of having multiple NSLocalizedString() calls with the same key.2 When your design changes and you need to update the base translation for a key, it’s much easier to update the single reference than it is to find and change all 28 of the references to that key in your code.

…but, What About Xcode 10?

Surprisingly, the localization changes in Xcode 10 don’t really impact the process of localizing your code. The new Xcode Localization Catalog packs a lot more information than what Xcode 9 provided, but it’s additive as opposed to replacing the old way of doing things.

Xcode Localization Catalog structure

Example of Xcode 10’s new Xcode Localization Catalog.

The same XLIFF file is right there, in the catalog’s Localized Contents folder. If your localizers understand how to view storyboards and read stringsdict files, the extra information is there for them. If not, they can continue to focus on the XLIFF file, the same as before.

When I recently tackled them localization of a new project, I realized just how much the process had changed in the intervening years since the last time I’d started from scratch. It took me a while to figure out why I couldn’t find that pesky Localizable.strings file anywhere, but I was quite happy once I actually wrapped my head around the impact of the changes. Even if you’re not currently translating your app into multiple languages, adopting best practices around localization can yield cleaner and more maintainable code.

  1. Although you typically can’t use beta version of Xcode to submit your apps, there’s nothing stopping you from using it when exporting and importing your translations. 

  2. Tools like SwiftGen or Laurine that can generate these centralized references, but they parse Localizabile.strings instead of directly processing the NSLocalizedString() calls. 

Eric Blair

MartianCraft Alumni

MartianCraft is a US-based mobile software development agency. For nearly two decades, we have been building world-class and award-winning mobile apps for all types of businesses. We would love to create a custom software solution that meets your specific needs. Let's get in touch.