Localization in Core Data
Let’s say, we have the following Core Data model:
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:
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:
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.