Friday, July 1, 2011

Dynamic translations in pure QML
























Anyone who develop in QML knows for a fact that it is a problem to translate those at runtime.

I am currently working on a new opensource project named qwazer which is a web client for Waze written in pure QML. I initially did it as a Hebrew & Israel only client, but then there was a lot of requests for multi-country/multi-lingual support.

Still wanting to stay in pure QML, it would seems that there was no way but to use the QtLinguist for translations and use QT/C++ calls.

I said no to that after several attempts and came up with a dynamic translations system where I can change the language with a click of a button from inside the application QML itself without involving any QT/C++ code.



Howto:
translator.js - This is uncomplete as I will add soon a mechanism that loads all translation files dynamically from other JS files as needed in order to avoid putting all translation in the same JS:

var _currentTranslation;
var _translations = [];
var _hebrewTranslation = {"Settings": "הגדרות", "Language%1": "שפה%1"};

function initializeTranslation() {
console.log("initialized translations");
_translations["en"] = {};
_translations["he"] = _hebrewTranslation;
_currentTranslation = _translations["en"];
}

function setLanguage(languageId) {
console.log("language set requested: " + languageId);
_currentTranslation = _translations[languageId];
console.log("language was set");
}

function translate(key, args) {

console.log("translating " + key);
var value = key;
if (typeof(_currentTranslation) != "undefined" && typeof(_currentTranslation[key]) != "undefined")
{
value = _currentTranslation[key];
}

if (typeof(args) != "undefined")
{
for(var i=0; i<args.length; i++)
value = value.replace(eval("/%"+(i+1)+"/"), args[i]);
}

return value;
}


Translator.qml:

import QtQuick 1.0
import "js/translator.js" as Translator

QtObject {

signal retranslateRequired(string langId)

function initializeTranslation() {
Translator.initializeTranslation();
}

function setLanguage(languageId) {
Translator.setLanguage(languageId);
retranslateRequired(languageId)
}

function translate(key, args) {
return Translator.translate(key, args);
}
}



root.qml:

Rectangle {
id: mainView
width: 800
height: 400

property string forceTranslate
onForceTranslateChanged: console.log("retranslation requested")

Connections {
target: translator
onRetranslateRequired: forceTranslateChanged()
}

Translator {
id: translator
}

...


settings.qml - translator and persistent configuration maintainer:

Rectangle {
id: qwazerSettings

signal settingsLoaded

function initialize() {
translator.initializeTranslation();
Storage.initialize();
qwazerSettings.state = "Loaded";
settingsLoaded();
}

...

onLanguageChanged : {
Storage.setObjectSetting("Language", language);
translator.setLanguage(language.langId);
retranslateRequired(language.langId);
}


qml texts that needs translating:

Text {
id: languageLabel
text: translator.translate("Language%1", ":") + mainView.forceTranslate
font.pointSize: 20
}


Explanation:
  • The translator is initialized at earlymost possible
  • Whenever the language changes, I call the event that will eventually set the current translation map
  • Translation and string format (denotes by %1, %2, etc...) is done by the translate function of the translator.
  • In order to reevaluate the translation, I concat an empty string and link between the language changed event and the empty string event - found it in a forum
  • The keys are actually the English translation - and that is why I set the English translations to empty