Working with Internationalization and Localization in swift

iOS, Swift, Localization, RTL/LTR

Part 2 available with a Library, Please use it instead from here :]
Source code updated to swift 3 in the Repo.Plus latest fixes and changes.
  • Apple Swift version 2.1.1 (swiftlang-700.1.101.15 clang-700.1.81)
  • Xode 7.1
  • iOS 9

Targets

  • User should be able to use another language from within the app.
  • Layout should responds to the change (RTL(Right to Left)/LTR(Left to Right)).
  • Using built in xliff generator.
  • StoryBoard with Base localization and .String compliments “no multiple story boards” .
  • simplicity.

I will use English and Arabic here, since English is LTR and arabic is RTL.

Project Setup

STEP ONE

Create new fresh Xcode project. Lets Name it Localization102

New single View Application
figure (1–3)

STEP TWO

Click on project file from the navigation area as in figure(1–3). Select Localization102 under PROJECT,figure(2–1).

figure(2–1)

Click on the plus sign under Localizations and add Arabic(ar), you will get screen listing files to be localized. click finish.

STEP THREE (Adding UI)

Lets add some UI elements in the storyboard to test with. We will have a UILabel, UIButton to switch language, UIImageView, UIButton for navigation, 2 UIViewControllers and one UINaviationcontroller.

Go to Main.Storyboard and From Object Library add a navigation controller and another UIViewController, set the UIViewController of class ViewController as the root of the navigation controller. Set the Navigation Controller as Initial View Controller from the Attribute Inspector.

They should look like figure(3–1).

figure(3–1)
figure(3–2)

Then add UILabel, an imageView, two buttons as in figure (3–2).

Its better to use an arrow image to test flipping effect when we go from RTL To LTR and vice versa. I am going to use this ARROW image.

STEP 4 (Localizing the Storyboard)

Now we have setup up our layout, lets localize it. First Click on the main.storyboard file and in the Utility Area -> file inspector -> Localization,

uncheck the Arabic box, you will get this message

Click remove. Then go to the same box again and check it again, you will get this message.

Click on Replace file, since we need to add the updated UI Objects we added to the storyboard.

Now Take a look at the main.storyboard(Arabic) file. it should look like this.

Before localization
After localization
First View in Arabic

Lets test the arabic version . From the Toolbar -> scheme -> edit Scheme ->Run -> options -> Application language -> choose Arabic.

Run the app

Verify that you get the arabic translation screens.

in order to get back to the english we can revert back and from Application Language choose English. Note that System Language means it depends on the language of the OS chosen by the user. but of course instead of going to the setting app and change the language we can always change that from the scheme for testing, as we did.

second View in Arabic (note the back button is on the right)

The text in the label apparently is flipped, because by default it’s TextAlignment is Natural(depends on the direction of the language), so its RTL now.

STEP 5 (Switch language)

Lets implement the switch language button so we changes the language not from the Scheme but from our app.

  • Create a Method and name it switchLanguage, and connect it to the switch Language button in the MainView.
@IBAction func switchLanguage(sender: UIButton) {
}
  • Reset the Application Language from Scheme to System Language.
  • Lets code
To be able to modify the current Language set by the user we need to use the “AppleLanguages” global key.

Add this class


// constants
let APPLE_LANGUAGE_KEY = “AppleLanguages”
/// L102Language
class L102Language {
/// get current Apple language
class func currentAppleLanguage() -> String{
let userdef = NSUserDefaults.standardUserDefaults()
let langArray = userdef.objectForKey(APPLE_LANGUAGE_KEY) as! NSArray
let current = langArray.firstObject as! String
return current
}
/// set @lang to be the first in Applelanguages list
class func setAppleLAnguageTo(lang: String) {
let userdef = NSUserDefaults.standardUserDefaults()
userdef.setObject([lang,currentAppleLanguage()], forKey: APPLE_LANGUAGE_KEY)
userdef.synchronize()
}
}

L102Language is responsible for getting/setting language from/in the UserDefaults.

  • Add these lines into the switchLanguage Method
if L102Language.currentAppleLanguage() == “en” {
L102Language.setAppleLAnguageTo(“ar”)
} else {
L102Language.setAppleLAnguageTo(“en”)
}

We check if the language is english then set it to be arabic and vice versa.

Lets try the app, build and run then tap on the switch language button.

Restart the app and you should be seeing the arabic version of our app.

“ you might not get the language switched in the second run, just try again. don’t worry its not a bug.”

So now we managed to change the language and provide localizable storyboard, but we have 2 main issues .

  1. we need to restart the app.
  2. UI Elements are not flipped to the right when language is Arabic.

ISSUE #1

If you go to the project directory in Finder you will find folders named like this “ar.proj” and inside them the localized resources.

When the app run and the System Language is Arabic(ar) the ar.proj bundle is used whenever the app asks for a localized resources. and so with other languages.

A bundle is a directory with a standardized hierarchical structure that holds executable code and the resources used by that code.[apple]

So we need to switch ‘lproj’ bundle in order to get it it localized without restarting the app.

One way to do that is through Swizzling, in order to use this technique we need to look for the method used to localize any string and change its implementation to consider our language preferences.

Note that bundles have direct effect in localization so we can look at NSBundle methods, we can find now that This method ->

/* Method for retrieving localized strings. */
- (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName NS_FORMAT_ARGUMENT(1);

is used whenever we try to localize a string, which makes it our target.

Now create a file and name it L012Localizer.swift, and these lines

class L012Localizer: NSObject {
class func DoTheSwizzling() {
// 1 
MethodSwizzleGivenClassName(NSBundle.self, originalSelector: Selector(“localizedStringForKey:value:table:”), overrideSelector: Selector(“specialLocalizedStringForKey:value:table:”))
}
}
extension NSBundle {
func specialLocalizedStringForKey(key: String, value: String?, table tableName: String?) -> String {
/*2*/let currentLanguage = L102Language.currentAppleLanguage()
var bundle = NSBundle();
/*3*/if let _path = NSBundle.mainBundle().pathForResource(currentLanguage, ofType: “lproj”) {
bundle = NSBundle(path: _path)!
} else {
let _path = NSBundle.mainBundle().pathForResource(“Base”, ofType: “lproj”)!
bundle = NSBundle(path: _path)!
}
/*4*/return (bundle.specialLocalizedStringForKey(key, value: value, table: tableName))
}
}
/// Exchange the implementation of two methods for the same Class
func MethodSwizzleGivenClassName(cls: AnyClass, originalSelector: Selector, overrideSelector: Selector) {
let origMethod: Method = class_getInstanceMethod(cls, originalSelector);
let overrideMethod: Method = class_getInstanceMethod(cls, overrideSelector);
if (class_addMethod(cls, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(cls, overrideSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
method_exchangeImplementations(origMethod, overrideMethod);
}
}
  1. we exchange the implementation of “localizedStringForKey:value:table:” with our “specialLocalizedStringForKey:value:table:”, passing the class which is NSBundle(.self to reference the object type).
  2. we get the preferred language.(e.g. en)
  3. we check if there is a bundle for that language (e.g. en.lproj), if not we user Base.proj, and we get store a reference in bundle var.
  4. we use localizedStringForKey:: method to return the localized method, note in swizzling we exchange the IMPlementation not the reference on the function, hence calling this method will use the original implementation of localizedStringForKey:: Method.

Now add this line in the Appdelegate in the didFinishLaunchingWithOptions delegate method.

L102Localizer.DoTheSwizzling()

OK NOW BUILD AND RUN :)

On the main screen Tap on “Switch button”.

Yeah nothing happened 😂

To solve this we need to reload our viewControllers . to do that add these lines at the end of the “switchLanguage” method, and in the Main.storyboard set the StoryboardId for the navigation controller to be “rootnav” .

let rootviewcontroller: UIWindow = ((UIApplication.sharedApplication().delegate?.window)!)!
rootviewcontroller.rootViewController = self.storyboard?.instantiateViewControllerWithIdentifier(“rootnav”)
let mainwindow = (UIApplication.sharedApplication().delegate?.window!)!
mainwindow.backgroundColor = UIColor(hue: 0.6477, saturation: 0.6314, brightness: 0.6077, alpha: 0.8)
UIView.transitionWithView(mainwindow, duration: 0.55001, options: .TransitionFlipFromLeft, animations: { () -> Void in
}) { (Bool finished) -> Void in
}

Build, run, and tap on SwitchLanguage button.

Now it should be working , when we switch the language, all the strings are changed with respect to the selected language.

Switching

SUM UP

Now we found a way to localize our storyboard, change the language, and update UI in runtime.

Next

in iOS9 the semantic changes as well depending on the language, but ! it happens only on the entrance of the app Just like bundle in issue#1.

Another issue is textAlignment in UILabel,TextField,TextView do not change automatically.

UIImages do not get flipped Automatically as well.

As you see in our project the UI Elements do not flip as well.

what about numbers!!!


AutoLayout

Autolayout not only help us building our Interface to adapt many screens, but also can help us in positioning the views correctly regarding the Language Direction, it does that by flipping the right and left Attributes of UILayoutConstraint. so what is a left Attribute in english is right attribute in arabic and so.

So Let add some constraints to our UI first.

BUILD AND RUN, tap on switch language button, close the app, run it again. You will see that the layout has been flipped.

ISSUE #2 (Mirroring)

As I mentioned above One of the main issues is the textAlignment, notice that setting the alignment as Natural in the Interface building for our UILabel does not flip the Text!!.

Mirroring User interface is part of internationalizing the app.

To solve that go to ViewController.swift and in switchLanguage Method update the if condition to look like this .

if L102Language.currentAppleLanguage() == “en” {
L102Language.setAppleLAnguageTo(“ar”)
UIView.appearance().semanticContentAttribute = .ForceRightToLeft
} else {
L102Language.setAppleLAnguageTo(“en”)
UIView.appearance().semanticContentAttribute = .ForceLeftToRight
}

in iOS9 we can force the semantics from the appearance. :) (we will get to how to make it work on iOS8 later )

run and try , you should get a result like this.

After forcing the semantics

So the now the Views are flipping and texts are correctly aligned! and thats all on runtime!!

UIImageView (Mirroring) (1)

You can see that the arrows are not flipping correctly. our custom image is not going to flip even after restarting the app, but the Back Navigation Bar button arrow IS flipped after restarting the app.

First Lets Mirror out own custom arrow image, for that we can use

public init(CGImage cgImage: CGImage, scale: CGFloat, orientation: UIImageOrientation)

initializer.

To make it more dynamic, instead of flipping each image a side, I created a superViewController that loops through its subViews and when it finds an image it flip it if its tag is less than 0 .

So lets create new UIViewController and Name it MirroringViewController .

And modify our ViewController to be a subClass of MirroringViewController.

class ViewController: MirroringViewController

Add these lines to that class

import UIKit
extension UIViewController {
func loopThroughSubViewAndFlipTheImageIfItsAUIImageView(subviews: [UIView]) {
if subviews.count > 0 {
for subView in subviews {
if subView.isKindOfClass(UIImageView.self) && subView.tag < 0 {
let toRightArrow = subView as! UIImageView
if let _img = toRightArrow.image {
/*1*/toRightArrow.image = UIImage(CGImage: _img.CGImage!, scale:_img.scale , orientation: UIImageOrientation.UpMirrored)
}
}
/*2*/loopThroughSubViewAndFlipTheImageIfItsAUIImageView(subView.subviews)
}
}
}
}
class MirroringViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if L102Language.currentAppleLanguage() == “ar” {
loopThroughSubViewAndFlipTheImageIfItsAUIImageView(self.view.subviews)
}
}
}
  • 1 ) is where flipping the image
  • 2) we call this function recursively to make sure we cover all subView.
  • This will not work on UITableView cells subViews. So you need to flip the cell images if needed inside the UITableView delegate.

UIImageView (Mirroring) (2)

Now lets solve the navigation back button arrow.

our loop Method did not work because the navbar is not a subView of our ViewController. UIBarbuttonItem /UINavigationItem are not of UIView type so we can’t reach them hierarchaly .

for that we need to use the userInterfaceLayoutDirection computed property in UIAplication class, as we know each app has one single UIApplication instance or a subclass of it , this instance has a big role in any app. one thing that interest us is the userInterfaceLayoutDirection.

This method specifies the general user interface layout flow direction

One way to update its implementation is by subclassing the UIApplication by adding main.swift file , and adding this line

UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(MyApp), NSStringFromClass(AppDelegate))
  • MyApp* is our custom subclass where we override the userInterfaceLayoutDirection property . like this
override var userInterfaceLayoutDirection: UIUserInterfaceLayoutDirection {
get {
var direction = UIUserInterfaceLayoutDirection.LeftToRight
if Languages.currentAppleLanguage() == “ar” {
direction = .RightToLeft
}
return direction
}
}

Another way is through the lovely Swizzling :) OF COURSE

add this line to the DoTheSwizzling() Method
-----------------------------------------------
MethodSwizzleGivenClassName(UIApplication.self, originalSelector: Selector(“userInterfaceLayoutDirection”), overrideSelector: Selector(“cstm_userInterfaceLayoutDirection”))

and this extension

extension UIApplication {
var cstm_userInterfaceLayoutDirection : UIUserInterfaceLayoutDirection {
get {
var direction = UIUserInterfaceLayoutDirection.LeftToRight
if L102Language.currentAppleLanguage() == “ar” {
direction = .RightToLeft
}
return direction
}
}
}

This method simply will check what is the language we are using and if it RTL we return .RightToLeft .

Lets celebrate by a successful run :)

  • as I noted userInterfaceLayoutDirection is called for every UIView whenever the view is updated or created. so we should not add a big time consuming functionality in it.
from now on we can use  UIApplication.sharedApplication().userInterfaceLayoutDirection
to check the direction of the app.

iOS8

As it mentioned above you can’t use UIView.appearance().semanticContentAttribute

in iOS 8, so lets remove those lines and run the app.

switch the app and go to next control and so, you will notice that the UILabel textAlignment is not aligned correctly on one of the languages unless you restart the app of course.

One way to solve it, is by creating Custom UILabel, lets call it MirroringLabel

Set the UILabel Class to be MirroringLabel and the tag to be -1.

inside MirroringLabel class, we need to override the layoutSubViews method , add these lines :-

override func layoutSubviews() {
// check if its already correctly aligned
if self.tag < 0 {
if UIApplication.isRTL() {
if self.textAlignment == .Right {
return
}
} else {
if self.textAlignment == .Left {
return
}}}
// if not align it based on the Direction , check first if the tag is less than 0 (which means we want this label to be directional not for example centered)
if self.tag < 0 {
if UIApplication.isRTL() {
self.textAlignment = .Right
} else {
self.textAlignment = .Left
}}
}

and add this helper method in UIApplication extensions inside L102Localizer.swift

extension UIApplication {
class func isRTL() -> Bool{
return UIApplication.sharedApplication().userInterfaceLayoutDirection == .RightToLeft
}
}

ok now, lets run again. you should be able to get the right behavior.

For UITextField, UITextView its the same story add a superclass for them and update the tags to be less than 0 (btw, this less than 0 thing can be anything you want. its just a criteria to determine if a UIView should flip its content or not)

Localizable.String

So we could localize the storyboard Texts, what about the dynamic strings we add in code/programatically or maybe you don’t even use storyboard.

its simple we need to add a Strings file and name it Localizable.string

  • Press CMD + N
  • under iOS -> Resource -> Choose Strings File
  • Name it Localizable and create
  • Click on the created file and from File Inspector, click on the Localize… button
  • When the “Do you want to localize this file?” window shows, click localize.
  • Check the Arabic option box.
  • you will get this look

We don’t need to add anything for now, lets do it in a nicer way.

As promised we will use the built in export/import xliff files feature in xcode.

Before that lets create another UILabel and connect it as an outlet in our ViewController, lets call it programmaticallylocalizedLabel (yeah long name but suitable), add this line at the start of ViewController.

@IBOutlet weak var programmaticallylocalizedLabel: UILabel!

Create a UILabel in the storyBoard, name it anything, and connect it to the outlet . Just make sure the tag is less than 0.

In ViewDidLoad() add this line

self.programmaticallylocalizedLabel.text = NSLocalizedString(“localize me please”, comment: “Localize me Label in the main scene”)

Its always a good idea to add a useful comments or Notes so the guy or you later understand what and why this text should be localized.

ok now The fun .

XLIFF Export/Import

  • Click on the blue project Under the project Navigator and then click on Editor from the Menu
  • Now Click on Export For Localization .
  • you can chose an existing Translations or Development Language Only
  • since we want to translate Arabic lets not change anything and click on save.
  • In The finder you will find the file named ar.xliff
  • There are many ways to translate this file, I like this website (http://xliff.brightec.co.uk/)
  • from the Main page choose the file which is ar.xliff and click on start translating.
  • its very great and useful service (hope it stays free :) )
  • You can see 9- Localize me please field, thats were we need to fill in.
  • write the localization you want and then Tap on the floating Green Button.
  • you will get new.xliff generated file
  • ok now go back to xcode and from the Editor in the Menu, click on “Import Localizations” and choose the generated file “new.xliff”
  • After that you will get this screen
  • Click on import
  • got to Localizable.strings the arabic version, you will see the localized string.
/* Localize me Label in the main scene */
“localize me please” = “لوكالايز مي”;
  • Run the app and see the localization working.
FINALE

Source Code

You can find the source code for the finished project here

FINALE

I hope you enjoyed this kinda long post. This post by now has done its Targets, well its not that simple, but it worth it!.

If you have a comment/question/Edit please add it, thank you.