How to unit test localizable strings in Swift

·

3 min read

How to unit test localizable strings in Swift

If you have ever worked on a project that supports more than one language, you have definitely been through the headaches of keeping Localizable.strings files consistent for different languages. This article tries to solve the most common problems with the help of unit tests.

Let's assume that, your project supports English (as default) and Italian. You most probably fall into one of the following traps:

  • You add a new word into en.lproj/Localizable.strings, but, you forget to update it.lproj/Localizable.strings
  • You add a new word into en.lproj/Localizable.strings and you put the same word into it.lproj/Localizable.strings just because Italian translation is not ready yet.
  • You delete a few words accidentally or modify any of above files which leads to an inconsistency.

Solution

Language - is an enum that groups the languages we are going to support.

enum Language: String, Codable {

    case english = "en"
    case italian = "it"

    // Alternatively, you can conform to CaseIterable and use allCases property instead
    static let supported: [Language] = [.english, .italian]

    static let defaultLanguage: Language = .english

    var displayName: String {
        switch self {
        case .english:
            return "English"
        case .italian:
            return "Italiano"
        }
    }
}

To open Localizable.strings file, we need to load language specific bundle.

func getBundle(for language: Language) -> Bundle? {
    guard let path = Bundle.main.path(forResource: language.rawValue, ofType: "lproj") else {
        return nil
    }

    return Bundle(path: path)
}

This function below loads Localizable.strings from given bundle and converts into dictionary

func getLocalizableDictionary(from bundle: Bundle) -> [String: String]? {
    guard let path = bundle.path(forResource: "Localizable", ofType: "strings") else {
        return nil
    }

    let dictionary = NSDictionary(contentsOfFile: path)
    return dictionary?.reduce(into: [String: String](), { $0[$1.key as! String] = $1.value as? String})
}

Here goes another helper function that simplifies the loading process

func getLocalizedDictionary(for language: Language) -> [String: String]? {
    guard let bundle = getBundle(for: language) else {
        return nil
    }

    return getLocalizableDictionary(from: bundle)
}

The fun part begins here.

    func test_translatedWords_basedOnDefaultLocale() {
        let defaultLanguage = Language.defaultLanguage
        let defaultLocalizedDictionary = getLocalizedDictionary(for: defaultLanguage)!
        let supportedLanguagesExceptDefault = Language.supported.filter { $0 != defaultLanguage }

        for language in supportedLanguagesExceptDefault {
            let localizedDictionary = getLocalizedDictionary(for: language)!

            for (key, defaultTranslation) in defaultLocalizedDictionary {
                if let translation = localizedDictionary[key] {
                    if translation == defaultTranslation {
                        debugPrint("⚠️Warning: '\(key)' has identical translations at \(defaultLanguage.displayName) and \(language.displayName)")
                    }
                } else {
                    XCTFail("'\(key)' is not translated into \(language.displayName)")
                }
            }
        }
    }

defaultLocalizedDictionary contains key-value pairs for default language as dictionary. In our example, It is English and it is also considered to be the source of truth.

Inside for-loop, we check if other language dictionaries contain all words from default language. If any word is not found in localizedDictionary, the test fails. If the language under test has identical word translation as in default language, a warning gets printed in the logs. In helps us to avoid the second problem mentioned in the beginning of the article.

Summary

This test prevents us from many localization related mistakes and lets us find missing pieces quickly. Feel free to give it a try in your project as well and let me know your thoughts.

Source code: LocalizationTests.swift