Localization in Core Data

Let’s say, we have the following Core Data model:

CoreData model: Expense and ExpenseCategory

This is a simple data model for expense tracking application: user adds his/her expenses and selects category for each expense.

Application should populate ExpenseCategory entities during the first run. For example, we add the following expense categories during the first start of the application:

  • Entertainment

  • Food

  • Insurance

  • Travel

  • Utilities

Now user can add expenses, and select categories from the list of predefined values. So far, so good. It’s time to bring our application to non-English speaking market, for example Ukraine. Ukrainians use Ukrainian language, which is completely different from English. Here is the same list of expense categories in Ukrainian:

  • Розваги

  • Їжа

  • Страхування

  • Подорожі

  • Комунальні послуги

How we can add localization to predefined list of entities in CoreData model?

NSLocalizedString

One can try to use string resources and NSLocalizedString function to get localized string values by keys. In this case keys will be attribute values from the storage.

One of the problems with this approach is that you can’t sort entities using NSFetchRequest  class, because it works with data in the storage which is not the data you display. When you want to filter your data, you should transform filter values from localized to original data in order to perform selection from the storage.

Property List

Here is another solution. We store original and localized data in a property list. During the application startup we check if application’s UI language changed from the previous run. If it changed, we update data with localized values.

OK. Let’s implement this.

Property List Structure

We should define how we organize localization data in the property list.

This is how I organize it:

CoreData localization: property list with initial data

Root node is a dictionary which keys are locale names (us, uk). Each locale node is a dictionary which keys are names of the CoreData entities for the application (ExpenseCategory). Each entity node is an array of dictionaries (Item 0, Item 1, …​). These dictionaries contains attribute values for CoreData entities (id, name, summary). We need id attribute for all data we want to localize, because this is a key which allows us to identify specific entity for update, we can’t use other attributes, because they could be changed during the localization.

Update Data Model

Add id:String attribute to ExpenseCategory entity in the data model:

CoreData model: Expense and ExpenseCategory with id attribute

Store Language Code in the Application’s Directory

We need an ability to read and to write language code, we used to populate storage with localization data during the last run of the application. Let’s store this code in the file. This file should live in the special application’s support directory: NSApplicationSupportDirectory. To get location of this directory we should use NSFileManager class:

/// Gets URL of directory where application can store its internal preferences.
private var preferencesDirectory: NSURL {
    get {
        let fileManager = NSFileManager.defaultManager()
        let applicationSupportDirectory = fileManager.URLForDirectory(.ApplicationSupportDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true, error: nil)!
        let preferencesDirectory = applicationSupportDirectory.URLByAppendingPathComponent(NSBundle.mainBundle().bundleIdentifier!, isDirectory: true)

        fileManager.createDirectoryAtURL(preferencesDirectory, withIntermediateDirectories: true, attributes: nil, error: nil)

        return preferencesDirectory
    }
}

Now we can add a code that reads/writes last language code into special file in the application’s support directory:

/// Gets or sets the language code, that was used to populate data storage with localized data during the last run
/// of the application
private var previousLanguageCode: String {
    get {
        // Get the name of the file which we use to store language code
        let fileURL = preferencesDirectory.URLByAppendingPathComponent("previousLanguageCode", isDirectory: false)

        var language = ""

        let fileManager = NSFileManager.defaultManager()
        if fileManager.fileExistsAtPath(fileURL.path!) {
            // Read language code
            language = String(contentsOfFile: fileURL.path!, encoding: NSUTF8StringEncoding, error: nil)!
        } else {
            // Do nothing.
        }

        return language
    }

    set {
        // Get the name of the file which we use to store language code
        let fileURL = preferencesDirectory.URLByAppendingPathComponent("previousLanguageCode", isDirectory: false)

        let fileManager = NSFileManager.defaultManager()
        // Write language code
        newValue.writeToFile(fileURL.path!, atomically: true, encoding: NSUTF8StringEncoding, error: nil)
    }
}

This property returns an empty string if we didn’t store language code yet. This should happen only once - during the first start of the application.

The next piece of code gets current UI language of the application:

/// Gets language code that is used by application right now
private var currentLanguageCode: String {
    get {
        let languages = NSLocale.preferredLanguages() as [String]
        return languages[0]
    }
}

The first language in the collection of preferred languages is the current language.

localize()

We are almost done. Here is a method that does localization of the data in the CoreData storage:

public func localize() {
    if currentLanguageCode != previousLanguageCode {
        // save current language code
        previousLanguageCode = currentLanguageCode
        updateOrCreateLocalizationData(currentLanguageCode)
    } else {
        // There is no need to update data that needs to be localized
    }
}

Now we have code that handles application language changes. When user changes iPhone language, all applications are terminated. During the next start-up, our application will update its localization data in the storage.

Update Entities in the Storage

The last method updates data in the storage according to the given language code:

/// Updates attributes of CoreData enitites using InitialData.plist and the given language.
private func updateOrCreateLocalizationData(languageCode: String) {
    // Load property list with localization data
    let initialDataPath = NSBundle.mainBundle().pathForResource("InitialData", ofType: "plist")!
    let initialData = NSDictionary(contentsOfFile: initialDataPath)!

    // Get dictionary with entities for the given language
    var languageSpecificData = initialData.objectForKey(languageCode) as NSDictionary?

    if languageSpecificData == nil {
        // There is no localization data for the given language. Fallback to English
        languageSpecificData = initialData.objectForKey("en") as NSDictionary?
    }

    // Now we should check if provided data exist and if it is - update it. If it is not - update it
    //
    // The method I use to do this has poor performance, but it is simple for understanding, that' why I use it here
    //
    // In the real world applications, one should use another methods.
    //
    // There is a good optimization guide here:
    // https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html
    for entitiesData in languageSpecificData! {
        // Get the name of the CoreData entity
        let entityName = entitiesData.key as String

        // Get attributes
        let entities = entitiesData.value as [[String: String]]

        let predicate = NSPredicate(format: "id == $ID")!
        for entity in entities {
            let entityId = entity["id"]!

            // Construct request that will look for the entity with the given ID
            let request = NSFetchRequest(entityName: entityName)
            request.predicate = predicate.predicateWithSubstitutionVariables(["ID": entityId])
            request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
            request.resultType = .ManagedObjectResultType

            // Try to find entity with the given ID
            let objects = moc.executeFetchRequest(request, error: nil) as [NSManagedObject]?

            var entityToUpdate: NSManagedObject? = nil

            if objects != nil && objects!.count > 0 {
                // Found. The code below will update it
                entityToUpdate = objects![0]
            }

            if entityToUpdate == nil {
                // There is no such data in the storage. Create it
                entityToUpdate = NSEntityDescription.insertNewObjectForEntityForName(entityName, inManagedObjectContext: moc) as? NSManagedObject
            }

            // Now we can fill entities attributes
            entityToUpdate?.setValuesForKeysWithDictionary(entity)

            moc.save(nil)
        }
    }
}

This code loads property list, finds localization data for the given language and then creates (or updates existing) CoreData entities in the storage.

Localizer Class

Let’s combine the whole code into the Localizer class:

import CoreData

public class Localizer {
    private let moc: NSManagedObjectContext

    public required init(managedObjectContext: NSManagedObjectContext) {
        moc = managedObjectContext
    }

    public func localize() {
        if currentLanguageCode != previousLanguageCode {
            // save current language code
            previousLanguageCode = currentLanguageCode
            updateOrCreateLocalizationData(currentLanguageCode)
        } else {
            // There is no need to update data that needs to be localized
        }
    }

    /// Gets or sets the language code, that was used to populate data storage with localized data during the last run
    /// of the application
    private var previousLanguageCode: String {
        get {
            // Get the name of the file which we use to store language code
            let fileURL = preferencesDirectory.URLByAppendingPathComponent("previousLanguageCode", isDirectory: false)

            var language = ""

            let fileManager = NSFileManager.defaultManager()
            if fileManager.fileExistsAtPath(fileURL.path!) {
                // Read language code
                language = String(contentsOfFile: fileURL.path!, encoding: NSUTF8StringEncoding, error: nil)!
            } else {
                // Do nothing.
            }

            return language
        }

        set {
            // Get the name of the file which we use to store language code
            let fileURL = preferencesDirectory.URLByAppendingPathComponent("previousLanguageCode", isDirectory: false)

            let fileManager = NSFileManager.defaultManager()
            // Write language code
            newValue.writeToFile(fileURL.path!, atomically: true, encoding: NSUTF8StringEncoding, error: nil)
        }
    }

    /// Gets language code that is used by application right now
    private var currentLanguageCode: String {
        get {
            let languages = NSLocale.preferredLanguages() as [String]
            return languages[0]
        }
    }

    /// Gets URL of directory where application can store its internal preferences.
    private var preferencesDirectory: NSURL {
        get {
            let fileManager = NSFileManager.defaultManager()
            let applicationSupportDirectory = fileManager.URLForDirectory(.ApplicationSupportDirectory,
                         inDomain: .UserDomainMask,
                appropriateForURL: nil,
                           create: true,
                            error: nil)!
            let bundleIdentifier = NSBundle.mainBundle().bundleIdentifier!
            let preferencesDirectory = applicationSupportDirectory.URLByAppendingPathComponent(bundleIdentifier, isDirectory: true)

            fileManager.createDirectoryAtURL(preferencesDirectory, withIntermediateDirectories: true, attributes: nil, error: nil)

            return preferencesDirectory
        }
    }

    /// Updates attributes of CoreData enitites using InitialData.plist and the given language.
    private func updateOrCreateLocalizationData(languageCode: String) {
        // Load property list with localization data
        let initialDataPath = NSBundle.mainBundle().pathForResource("InitialData", ofType: "plist")!
        let initialData = NSDictionary(contentsOfFile: initialDataPath)!

        // Get dictionary with entities for the given language
        var languageSpecificData = initialData.objectForKey(languageCode) as NSDictionary?

        if languageSpecificData == nil {
            // There is no localization data for the given language. Fallback to English
            languageSpecificData = initialData.objectForKey("en") as NSDictionary?
        }

        // Now we should check if provided data exist and if it is - update it. If it is not - update it
        //
        // The method I use to do this has poor performance, but it is simple for understanding, that' why I use it here
        //
        // In the real world applications, one should use another methods.
        //
        // There is a good optimization guide here:
        // https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html
        for entitiesData in languageSpecificData! {
            // Get the name of the CoreData entity
            let entityName = entitiesData.key as String

            // Get attributes
            let entities = entitiesData.value as [[String: String]]

            let predicate = NSPredicate(format: "id == $ID")!
            for entity in entities {
                let entityId = entity["id"]!

                // Construct request that will look for the entity with the given ID
                let request = NSFetchRequest(entityName: entityName)
                request.predicate = predicate.predicateWithSubstitutionVariables(["ID": entityId])
                request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
                request.resultType = .ManagedObjectResultType

                // Try to find entity with the given ID
                let objects = moc.executeFetchRequest(request, error: nil) as [NSManagedObject]?

                var entityToUpdate: NSManagedObject? = nil

                if objects != nil && objects!.count > 0 {
                    // Found. The code below will update it
                    entityToUpdate = objects![0]
                }

                if entityToUpdate == nil {
                    // There is no such data in the storage. Create it
                    entityToUpdate = NSEntityDescription.insertNewObjectForEntityForName(entityName,
                        inManagedObjectContext: moc) as? NSManagedObject
                }

                // Now we can fill entities attributes
                entityToUpdate?.setValuesForKeysWithDictionary(entity)

                moc.save(nil)
            }
        }
    }
}

Integrate Localizer into the Application

Are you still here? Good :] The final piece of code integrates Localizer class into our application. Update AppDelegate class with the following:

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        let localizer = Localizer(managedObjectContext: managedObjectContext!)
        localizer.localize()

        return true
    }

The End

That’s it. I hope this was useful for you.