martes, 17 de marzo de 2015

Localizing Android Games in Cocos2d-x

Hi all!

I'm polishing the last details of my new game to come, and I lastly finished localizing the game in the Android platform. You can also check this post where I explained the steps to localize a game in iOS. 

I'm implementing my game in Cocos2d-x so I had two choices to implement localization:
- Reading the current language of the device from Cocos2d-x and provide a custom LocalizedString C++ class in order to translate each string to the corresponding language.
- Using the built-in localization mechanism in Android through the Java Native Interface (JNI). 

Since I've been learning recently JNI and I wanted to experiment more with this technology, I opted for the latter.  Here you can find tips to follow the first path. 

Localization in Android via Resources

Localization in Android is typically done by means of resources, as explained here. In my case, I only want to localize strings, so I'll let images and other types of resources aside. 

In the /res directory of your project, you find a sub-directory called /values. In turn, this directory contains an XML file called strings.xml. This is the place where all the strings to be localized are located. For example, an excerpt of my strings.xml is shown next:

 <resources>  
   ...
    <string name ="Accuracy">Accuracy</string>  
    <string name = "Art">Art</string>  
    <string name = "Programming">Programming</string>  
    <string name = "Music">Music</string>  
   ...
 </resources>  

We are specifying that the default value of the string "Accuracy" is Accuracy, and so on with the others. Say that we want to localize in Spanish. In that case, we will create a new sub-directory under /res with the name values-es, and we will create a strings.xml file with the following contents:

 <resources>  
   ...
    <string name ="Accuracy">Precisión</string>  
    <string name = "Art">Arte</string>  
    <string name = "Programming">Programación</string>  
    <string name = "Music">Música</string>  
   ...
 </resources>  

Upon the launch of the app, Android will find out the locale (language configuration) of the device and will determine whether to use the Spanish localization or the default localization. The key here is the suffix that we append to values directory. "-es" is the international suffix for Spain, whereas "-de" is the international suffix for Germany, for example. This is how Android knows in which strings.xml to look up. You can check the international suffices for languages and countries here and here.

Once we have our resources ready, we can access them programmatically from an Activity as follows:

String localizedString = getString( R.string.Accuracy );

The above statement would provide the variable localizedString with the value "Accuracy" in a device with English locale, and with the value "Precisión" in a device with a Spanish locale. R.string.Accuracy is actually an automatic identification number (integer) that Android generates for you in the auto-generated R.java file. The important thing here is that if you want to access any resource, you need to retrieve first its identification number.

Localizing in Cocos2d-x via JNI

In order to localize in Cocos2d-x, we need to communicate from the game logic (written in C++) to the main activity, written in Java. First, I added a new static method to the main activity (AppActivity.java):

 public static String getLocalizedString( byte[] b )  
 {  
     String str = "";  
     try {  
         str = new String( b, "UTF-8" );  
     } catch (UnsupportedEncodingException e) {  
         e.printStackTrace();  
     }  
     int id = _appActivity.getResources().getIdentifier( str, "string", _appActivity.getPackageName() );  
     String res;  
     if ( id == 0 )  
     {  
         res = str;  
     }  
     else   
     {  
         res = _appActivity.getString( id );  
     }  
     return res;  
 }  

Note that the method receives a byte array, which is converted to a UTF-8 string (since this is what I'll be sending from C++). This string is then used to retrieve the identification number of the resource (getIdentifier() method), and this identification number is in turn used to retrieve the actual localized string (getString() method). In case that a wrong identification number is found (id == 0), I return the original string. Also note that all the method calls concerning the activity are accessed through _appActivity, which is a static variable that holds a reference to the main activity. We need to to do this because otherwise we would lose the reference of the activity once the onCreate() method finishes.

Now that we have this method, we only need to make the corresponding call from the game logic when it is required. For this, I made a simple utility C++ class that manages the localization in the C++ side:

LocalizationManager.h

 #ifndef __Limball__LocalizationManager__  
 #define __Limball__LocalizationManager__  
 #include <string>  
 class LocalizationManager  
 {  
 public:  
   static LocalizationManager* GetInstance();  
   static void DestroyInstance();  
   std::string GetLocalizedString( const std::string& key );  
 private:  
   LocalizationManager();  
   ~LocalizationManager();  
   LocalizationManager( const LocalizationManager & ) = delete;  
   LocalizationManager& operator=( const LocalizationManager & ) = delete;  
   static LocalizationManager* lm;  
 };  
 #endif   

LocalizationManager.cpp

 #include "LocalizationManager.h"  
 #include "cocos2d.h"  
 #include "platform/android/jni/JniHelper.h"  
 #include <jni.h>  
 LocalizationManager* LocalizationManager::lm = nullptr;  
 LocalizationManager* LocalizationManager::GetInstance()  
 {  
   if (!lm)  
   {  
     lm = new LocalizationManager();  
   }  
   return lm;  
 }  
 void LocalizationManager::DestroyInstance()  
 {  
   delete lm;  
 }  
 LocalizationManager::LocalizationManager() {}  
 LocalizationManager ::~LocalizationManager()  
 {  
   lm = nullptr;  
 }  
 std::string LocalizationManager::GetLocalizedString( const std::string& key )  
 {  
     cocos2d::JniMethodInfo t;  
     jstring res;  
     if (cocos2d::JniHelper::getStaticMethodInfo(t, "org/cocos2dx/cpp/AppActivity", "getLocalizedString", "([B)Ljava/lang/String;"))  
     {  
         jbyteArray bArray = t.env -> NewByteArray( key.length() );  
         jbyte bytes[50];  
         for( int i = 0; i < key.length(); ++i )  
         {  
             bytes[i] = key[i];  
         }  
         t.env -> SetByteArrayRegion( bArray, 0, key.length(), bytes );  
         res = (jstring) t.env -> CallStaticObjectMethod( t.classID, t.methodID, bArray );  
         t.env -> DeleteLocalRef(t.classID);  
         t.env -> DeleteLocalRef( bArray );  
     }  
     return cocos2d::JniHelper::jstring2string( res );  
 }  

As an utility class, it is implemented following the Singleton pattern. The most interesting method is the one that actually calls the Java method defined earlier: GetLocalizedString(). First, by means of the JniHelper utility class provided by Cocos2d-x, we retrieve information about the static method that we defined in the AppActivity.java file. Then, we allocate memory for a byte array and we fill this byte array with the contents of the string passed as an argument. Next we call the static method and receive the result of type jstring. After releasing the memory allocated deleting the local reference, we convert the jstring to std::string and return the result of this conversion.

At this point, some of you may wonder: why do you use a byte array to pass a string to Java? Isn't there any other better way? Well, actually, it exists the function NewStringUTF( const char * ), which receives a C (null-terminated) string and returns a jstring that can be passed to Java directly as a regular Java String. However, as explained here, there is a bug from Android 4.0 and above that may cause app crashes, and this is why a byte array is the most recommended way to deal with handing strings to Java.

Now, the client code can use it as in the following example:

 std::stringstream ss1;  
 ss1 << accuracy;  
 std::string total( LocalizationManager::GetInstance() -> GetLocalizedString("Accuracy") + ": +" + ss1.str());  
 accuracyLabel = Label::createWithTTF(total, "fonts/GILSANUB.TTF", ConfigManager::GetInstance() -> GetFontSizeForScoreScreen() );  
 accuracyLabel -> setPosition(Vec2( accuracyLabel -> getContentSize().width / 2, visibleSize.height/ 3 ));  
 this -> addChild(accuracyLabel);  

Hope you found this post useful and enjoyable. In the next post, I expect to show a promotional video for the game I'm working on.

LocalizationManager::GetInstance() -> GetLocalizedString("See you!!")

EDIT: After researching a bit more, I noticed that I made a mistake in the LocalizationManager::GetLocalizedString() method. In particular, when you invoke NewByteArray(), you're creating a local reference. In a native method (a method that has been called from a Java environment), this local reference is automatically deleted when the the C++ method returns. However, given that we're not in a native method, we have to remove the reference manually by calling DeleteLocalRef(). Also, before I called ReleaseByteArray(), but this is only required when you call GetByteArray(). I changed the code accordingly. You can find references here and here.