Hi all!
I've been playing around with localization lately and I want to share what I learned.
First of all, I've tried to be very specific in the title. This post is about externalizing strings to external files instead of hard-coding them. This means that I won't deal here with images and other types of files localization. Neither will I discuss here internationalization, which is a broader topic.
Picture from Freemake.com (http://www.freemake.com/blog/interview-with-founder-of-crowdin-net/)
Let's suppose that we're creating a super fantastic game and we want, at the beginning, to show a welcome message to the players: "Welcome!"
In a non-localized app, you'd do something as simple as the following (in C++):
std::cout << "Welcome!; (Note: we'd actually embed this string in a label or other high-level structure that presents the information on the device screen rather than on the IDE console).
Our goal is that the message is shown in the language that players have configured in their devices. How do we achieve this? The overall idea is the following: we create a special kind of file (with .strings extension) and we localize it, meaning that Xcode will take care of creating different directories for each language. Then, in our code, instead of hard-coding strings like in the previous example, we make references to these strings in the .strings file. Finally, when the app is launched in the device, the app will be able to pick the correct language depending on the device settings.
The process can be divided in two steps, the first one focused on configuration and the second one on programming.
1. Configuring the environment
In Xcode (note that I'm using Xcode 5.1), choose:
1) File -> New -> File...
2) In the iOS section, choose Resource and Strings File.
3) Name the new file as Localizable.strings (this is a default name; you can change it, but then each time you want to access the file, you have to provide the name).
4) Save the file in some of your app directories.
5) Select the file and in File Inspector, click in Localize. Choose the languages that you want to support. You can add new languages at any time.
6) Even if you haven't seen it, Xcode has created as many folders as the number of languages. These folders follow the same name convention: en.lproj (for English localized strings), es.lproj (for Spanish localized strings), etc. Each language has a unique code. You can check all the codes here.
7) I advise you to drag these folders to Xcode, uncheck the option "Copy items into destination group's folder (if needed)" and to check "Create folder references for any added folders". This way, your file system is synchronized with what you see in Xcode and any changes are reflected in one another.
Now, you can fill out these files according to the following format:
"key" = "value";
Following the welcome example, in the Localizable.strings under the en.lproj folder, we would write:
"WelcomeMessage" = "Welcome!";
Whereas in the Localizable.strings under the es.lproj folder, we would write:
"WelcomeMessage" = "¡Bienvenido!";
And so on with the rest of languages that we want to support. Don't forget the ";" at the end.
2. Programming
Now we're ready to make the appropriate changes to the code. In Objective-C, this is pretty straightforward as the Cocoa framework offers localization methods. In particular, we're interested in the following one:
NSLocalizedString("key", nil)
This method returns a NSString* with the value associated to the NSString passed as first argument. Therefore, if we called the method as NSLocalizedString("WelcomeMessage", nil), it would return "Welcome!" in English-configured devices, and "¡Bienvenido!" in Spanish ones.
The problem is that given that we're assuming that we're using C++, we cannot directly access this method. For example, in my case I'm working with Cocos2d-x, which is a C++ framework. What can we do to access this Cocoa functionality from C++?
Well, given that Objective-C and C++ have common roots, it's not difficult to pave a bridge between them. Therefore, I created a C++ wrapper over the aforementioned Objective-C method. The code of the header and implementation files are listed next:
LocalizationManager.h
#ifndef __Limball__LocalizationManager__
#define __Limball__LocalizationManager__
#include <iostream>
#include <string>
class LocalizationManager
{
public:
static LocalizationManager* getInstance();
std::string getLocalizedString(const std::string& key);
private:
LocalizationManager();
~LocalizationManager();
static LocalizationManager* lm;
};
#endif
LocalizationManager.mm
#include "LocalizationManager.h"
LocalizationManager* LocalizationManager::lm = nullptr;
LocalizationManager* LocalizationManager::getInstance()
{
if (!lm)
{
lm = new LocalizationManager();
}
return lm;
}
LocalizationManager::LocalizationManager()
{
}
LocalizationManager ::~LocalizationManager()
{
if (lm)
{
delete lm;
}
}
std::string LocalizationManager::getLocalizedString(const std::string& key)
{
NSString *str = [NSString stringWithUTF8String: key.c_str()];
std::string s([NSLocalizedString(str, nil) UTF8String]);
return s;
}
What I do in the getLocalizedString method is basically transforming from NSString to std::string (using an intermediate C-style string), and viceversa. Note that the implementation file has no .cpp extension, but .mm extension. This is important as this file must be able to understand both C++ and Objective-C. You must change the type of this file to Objective-C++ Source in the File Inspector.
Now, in any class that needs to print a message, I only need to include the LocalizationManager.h, and I can use it as follows:
std::cout << LocalizationManager::getInstance() -> getLocalizedString("WelcomeMessage");
And that's all! Hope you find it useful.
FM