[tutorial] Multiple language support

Here I want to share multiple language mechanism that I use for my games.

Preface

The main idea is to store text file with translation data as a resource. During runtime we are going to load appropriate language file and get required string by its unique key.

LanguageManager implementation

For that purposes lets create shared singleton class LanguageManager:

#ifndef LanguageManager_h
#define LanguageManager_h

#include <string>
using std::string;

#include "cocos2d.h"
USING_NS_CC;

#include "cocos-ext.h" 
using namespace rapidjson; // library that we use for json parsing

class LanguageManager
{
    Document document; // current document with language data
    LanguageManager(); // constructor is private
    static LanguageManager* _instance;
public:
    static LanguageManager* getInstance();
    string getStringForKey(string key);
};
#endif

And the source file:


#include "LanguageManager.h"

LanguageManager* LanguageManager::_instance = 0;

LanguageManager::LanguageManager()
{
    string fileName;
    // detect current language
    switch(CCApplication::sharedApplication()->getCurrentLanguage()) 
    {
    case kLanguageEnglish:
        fileName = "en.json";
        break;
    case kLanguageRussian:
        fileName = "ru.json";
        break;
    default:
        CCLOG("Unknown language. Use english");
        fileName = "en.json";
        break;
    };

    // below we open, read and parse language data file with rapidjson library
    unsigned long size;
    const char* buf = (const char*)CCFileUtils::sharedFileUtils()->getFileData(fileName.c_str(), "r", &size);
    string content(buf);
    string clearContent = content.substr(0, content.rfind('}') + 1);

    document.Parse<0>(clearContent.c_str());
    if(document.HasParseError())
    {
        CCLOG("Language file parsing error!");
        return;
    }
}

LanguageManager* LanguageManager::getInstance()
{
    if(!_instance)
        _instance = new LanguageManager();
    return _instance;
}

string LanguageManager::getStringForKey(string key)
{
    return document[key.c_str()].GetString();
}

Usage example:

There is an example of LanguageManager usage from my project:

CCLabelTTF* pauseLabel = CCLabelTTF::create(
 LanguageManager::getInstance()->getStringForKey("pause_layer_pause_title").c_str(),
            fontName,
            titleFontSize);
    pauseLabel->setAnchorPoint(CCPoint(0.5, 0.5));
    pauseLabel->setPosition(CCPoint(
            0.5 * visibleSize.width,
            0.65 * visibleSize.height) + origin);
    addChild(pauseLabel);

Languange data file example:

en.json

{
    "main_menu_play_button_label":"Play",
    "mode_selection_menu_label":"Select mode",
    "pause_layer_pause_title":"Pause"
}

ru.json

{
    "main_menu_play_button_label":"Играть",
    "mode_selection_menu_label":"Выберете режим",
    "pause_layer_pause_title":"Пауза"
}

LanguageManager is adapted for version 2 but can be easily modified for version 3.

14 Likes

Denis, this is awesome!!

I like it!!!
Thank you for your contribution.

I adapted code for version 3.2. Source is attached.

Screenshots:

Source

1 Like

Nice! I’ve been using same concept in my games. One thing I would improve is:

replace this

 unsigned long size;
const char* buf = (const char*)CCFileUtils::sharedFileUtils()->getFileData(fileName.c_str(), "r", &size);
string content(buf);
string clearContent = content.substr(0, content.rfind('}') + 1);

with this

string clearContent = FileUtils::getInstance()->getStringFromFile(fileName);

Well done!

@Den
My game sometimes takes json successfully and sometimes it provides this error
I am using cocos2dx 2.2.5
``
Cocos2d: Language file parsing error!
Assertion failed: (IsObject()), function FindMember, file /Users/rajanmaheshwari/Documents/cocos2d-x-2.2.5/extensions/CocoStudio/Json/rapidjson/document.h, line 620.```

I am unable to understand why this is happening.out of 7 times it crashes 3 times with the above error message and sometimes it takes everything successfully.

I was a little altered under version 3.x Сoсos2d-x a little easier to use.

    #include <string>
using std::string;

#include "cocos2d.h"
USING_NS_CC;

#include "external\json\document.h" 
#include "external\json\rapidjson.h"

using namespace rapidjson; // library that we use for json parsing

class LanguageManager
{
    Document document; // current document with language data
    LanguageManager(); // constructor is private
    static LanguageManager* _instance;
public:
    static LanguageManager* getInstance();
    string getStringForKey(string key);
	static string getString(string key);    	
};
#endif

cpp:

  #include "LanguageManager.h"

LanguageManager* LanguageManager::_instance = 0;

LanguageManager::LanguageManager()
{
    string fileName;
    // detect current language
	switch(CCApplication::getInstance()->getCurrentLanguage()) 
    {
    case LanguageType::ENGLISH:
        fileName = "en.json";
        break;
	case LanguageType::RUSSIAN:
        fileName = "ru.json";
        break;
	case  LanguageType::CHINESE:
		fileName = "cn.json";
        break;
	case  LanguageType::FRENCH:
		fileName = "fr.json";
        break;
	case  LanguageType::ITALIAN:
		fileName = "it.json";
        break;
	case  LanguageType::GERMAN:
		fileName = "ge.json";
        break;
	case  LanguageType::SPANISH:
		fileName = "sp.json";
        break;
	case LanguageType:: DUTCH:
		fileName = "du.json";
        break;
	case  LanguageType::KOREAN:
		fileName = "ko.json";
        break;
	case  LanguageType::JAPANESE:
		fileName = "jp.json";
        break;
	case  LanguageType::HUNGARIAN:
		fileName = "hu.json";
        break;
	case  LanguageType::PORTUGUESE:
		fileName = "pt.json";
        break;
	case  LanguageType::ARABIC:
		fileName = "ar.json";
        break;
	case  LanguageType::NORWEGIAN:
		fileName = "nw.json";
        break;
	case  LanguageType::POLISH:
		fileName = "po.json";
        break;
    default:
        CCLOG("Unknown language. Use english");
        fileName = "en.json";
        break;
    };

    // below we open, read and parse language data file with rapidjson library
	string clearContent = FileUtils::getInstance()->getStringFromFile(fileName);
   
    document.Parse<0>(clearContent.c_str());
    if(document.HasParseError())
    {
        CCLOG("Language file parsing error!");
        return;
    }
}

LanguageManager* LanguageManager::getInstance()
{
    if(!_instance)
        _instance = new LanguageManager();
    return _instance;
}

string LanguageManager::getStringForKey(string key)
{
    return document[key.c_str()].GetString();
}

string  LanguageManager::getString(string key)
{
	return getInstance()->getStringForKey(key);
}

Sample use:

CCLabelTTF* pauseLabel = CCLabelTTF::create(
 LanguageManager::getString("pause_layer_pause_title").c_str(),
            fontName,
            titleFontSize);
1 Like

If you look for a way to use getStringForKey in a variadic way (to support variables in your i18n strings), you can use following impl.

std::string LanguageManager::getStringForKey(std::string key, ...) {

std::string ret;
std::string ss;
int n, size=100;
bool b=false;
va_list marker;

std::string fmt = document[key.c_str()].GetString();

while (!b)
{
    ss.resize(size);
    va_start(marker, key);
    n = vsnprintf((char*)ss.c_str(), size, fmt.c_str(), marker);
    va_end(marker);
    if ((n>0) && ((b=(n<size))==true)) ss.resize(n); else size*=2;
}
ret += ss;

return ret;
}

@noomieware,

Thank you, that is really great improvement!

Has anyone benchmarked RapidJSON used here? Something tells me that using RapidJSON’s document to extract the nodes in real-time is not efficient. Probably it would be better to scan the whole document and extract all the key-value pairs into unordered_map.
Any thoughts?

@Den @rajan About the crash rajan is having I got the same and managed to correct it.

In LanguageManager(), the getFileData function sometimes return the json file content followed with some random datas at the end (really don’t know why… ôo).

The content.substr(0, content.rfind(’}’) + 1) is supposed to end the String after the last occurence of ‘}’. However the random extra datas sometime include ‘}’ so the file can’t be parse properly and crash.
You just need to replace “rfind” by “find” to get the first occurence and remove all the extra datas.

Thanks Den for the tuto ;).

hi @dotsquid

Your post out of all these grabbed my attention.
I would also like to know, if it would be efficient to use rapidjson everytime for every single label object ?

I believe as this post is a little old.
You must have came to a conclusion.

I am really excited to hear from you.
Thanks in advance. :smile:

Hi @Den

I am pretty sure that the source code that you have attached will work like charm.

Just one thing that I want to know -

How the Application::getInstance()->getCurrentLanguageCode()
will return the current language.

Say, I am making my app and publishing it to the playstore and appstore.
Depending on the region of download, how will it automatically load those language… ??

Do, I need to have different builds… !!
I prefer not to have any language selection…

I am not clear on this… could you help me please… :smile:

Hello Rajan, I am using cocos2d-x v3.4 and having the same issue as of yours, I am unable to read whole .json file,while it is working fine if data inside .json file is small, Have you found any solution to that problem.

@Den , do you know what it (@rajan 's problem) is?. I’m using cocos2d-x v3.7. In my case I could run successfully once.

getStringFromFile does not bring Spanish(including char like ‘È’) correctly, any idea?

Short question :wink:

I use at this time this variables to handle all my wording in one file…
#define HL_WORLD "My World Headline"
etc…


is it possible to use it via *json?
like this:

at konstanten.h
#define HL_WORLD "_loc_WorldHeadline"

at level.cpp

//HEADLINE
notificationWorldHL -> setString(HL_WORLD);'

and than seach at *.json

{
 "_loc_WorldHeadline" : "My World Headline",
    "_loc_test1" : "Test 1!"  
}

So that I can use the a placeholder/variable for more automatism HL_WORLD?

thx :wink:

This WORKS :wink: Great :wink: -> (LanguageManager::getInstance()->getStringForKey(HL_WORLD).c_str())
notificationWorldHL -> setString(LanguageManager::getInstance()->getStringForKey(HL_LOOKED_WORLD).c_str());

Other question.

How can I solve a crash if one line with a translation is missing???
At this time the app crash… is it possible to grab the default lang (englisch) or something else? A crash is bad.