Projects with multiple targets often share resources, including localized resources. This is convenient, but it can also lead to problems down the road. In some situations, you want to provide custom translations for one or more targets and use the project’s default translations as a fallback. As far as I know, Cocoa applications don’t support this out of the box. But the solution isn’t too difficult to implement.

Setting Up the Project

Open Xcode and create a new project based on the Single View Application template.

Project Setup

Name the project and set Language to Swift.

Project Setup

Tell Xcode where you would like to save the project and click Create.

Project Setup

Add a new file to the project by choosing the Strings File template and name the file Default.

Add Strings File

With Default.strings selected, open the File Inspector on the right and click the Localize button to localize the resource.

Localize Strings File

Select English from the list of options and click Localize.

Localize Strings File

Add the following keys and values to the strings file. Make sure you add the translations to each translation file if you are using multiple localizations.

"key_a" = "This is a default translation.";
"key_b" = "This is a default translation.";

Create another strings file and name this one Localizable. Localizable.strings is the name Xcode expects and looks for. This becomes clear in a moment. Localize the file and add the following key and value to the strings file.

"key_b" = "This is an overridden translation.";

Project Structure

Fetching Translations

The idea is simple. We first want the application to look for translations in Localizable.strings. If the key it is looking for is not present in Localizable.strings, it should fall back to Default.strings.

There are several solutions to this problem. I have opted for a global function, global to the module, since you want this function to be available throughout your project.

func localizedString(forKey key: String) -> String {
    var result = Bundle.main.localizedString(forKey: key, value: nil, table: nil)

    if result == key {
        result = Bundle.main.localizedString(forKey: key, value: nil, table: "Default")
    }

    return result
}

The localizedString(forKey:) function accepts one argument of type String, the key for the translation, and it returns a value of type String, the translation. The implementation isn’t difficult. Let me break it down.

We ask the main bundle of the application for the localized string for the key that is passed to the localizedString(forKey:) function. We don’t provide a value or a table name, which means the operating system defaults to Localizable.strings. The table name is the name of the strings file.

var result = Bundle.main.localizedString(forKey: key, value: nil, table: nil)

If no translation can be found for the key that is passed in, the return value of the localizedString(forKey:value:table:) method is the key itself. That is what we check in the if statement. If the value of result is equal to the value of key, we know the application was not able to find a translation for the key.

if result == key {
    result = Bundle.main.localizedString(forKey: key, value: nil, table: "Default")
}

In that scenario, we fall back to Default.strings. We use the same method. But this time we pass Default as the last argument of the localizedString(forKey:value:table:) method because we are interested in the contents of Default.strings. We return the value of result from the function.

Testing the Implementation

Open ViewController.swift and update the implementation of viewDidLoad() as shown below.

override func viewDidLoad() {
    super.viewDidLoad()

    print(localizedString(forKey: "key_a"))
    print(localizedString(forKey: "key_b"))
}

Run the application and inspect the output in the console. This is what the output should look like.

This is a default translation.
This is an overridden translation.

More Complexity

This is an example of what your implementation could look like. You can include multiple strings files and provide several fallbacks. But for most projects, one fallback suffices.