Adding Live Translation to BlackBerry 10 C++ Code

Code Samples

MartinGreen

Editor:  Today we have a guest post from Martin Green.  Martin spent nearly a decade repairing marine navigational equipment on “Lakers” and “Salties”, such as RADAR, Loran C, gyrocompasses, and early SatNav, followed by twenty-five years as a contract Enterprise software developer for governments, banks, and major corporations before coming to mobile and BlackBerry 10 Native development in early 2013. He is the owner of arsMOBILIS and the developer of multiFEED, an RSS/Atom reader app available in BlackBerry World.

If you have built (or are building) your BlackBerry 10 Native Cascades apps with internationalization in mind then you likely have code peppered with QML qsTr() calls and C++ tr() macros. When your app starts up the text wrapped in qsTr() or tr() is substituted with the localized text for the device locale and language (if defined, otherwise default translation is used). This works fine unless you change the language setting on your device while your app is running. Unless you take steps to propagate the language change through your live app, the text displayed in your UI will not match the new device language setting until the page is reloaded or your app is restarted. QML provides for this “live translation” with the Retranslate class which can be used with qsTr() to update translations as soon the device language setting is changed:

Page {
    Container {
        Label {
         id: label01

         // ---This label's text will be live translated
         text: qsTr("Label 1") + Retranslate.onLanguageChanged
      }
      Label {
         id: label02

         // ---This label's text will be static translated
         text: qsTr("Label 2")
      }
   }
}

In this example label01 will have its text attribute live translated so that it is updated as soon as the device language is changed. On the other hand label02 will only have its text translated when the app is first started and subsequent language changes while the app is running will not update it.

Using the Retranslate class with QML makes live translation as simple as adding a small amount of code right after every use of qsTr(), but C++ provides no such convenience. To implement live translation in C++ code it is necessary to call the setXXX() slot for a string attribute with the tr() macro once to do the initial translation then connect the setXXX() slot to a LocaleHandler instance which emits a signal every time the language (or locale) changes. This is complicated by the fact that the LocaleHandler knows that the language has changed, but it doesn’t know which text key to supply to the slots it is attached to so we have to attach the LocaleHandler signal to an intermediary slot which knows what translation key to use for that attribute and which then emits another signal with the key as a parameter which must be connected to the control setXXX() slot. This means that for every single control attribute you want to live translate you will need a separate intermediary slot defined somewhere, a related signal for that slot with a QString argument, and two QObject::connect() calls. For every attribute we want to live translate. Obviously this gets very ugly, very fast.

I prefer to use C++ with little or no QML to lay out my app pages so I was determined to find a solution which was as easy to use as Retranslate in QML (or nearly so) and after some trial and error I came up with a solution I’m very happy with, which I called LiveTranslator. The core of this technique is a new C++ class called LiveTranslator. The usage syntax is a little more complicated than using QML Retranslate, but like Retranslate you only need to add one line of code for each attribute you want live translation on. Here is the header for LiveTranslator:

#ifndef LIVETRANSLATOR_HPP_
#define LIVETRANSLATOR_HPP_

#include <bb/cascades/LocaleHandler>

using namespace bb::cascades;

class LiveTranslator: public QObject {
    Q_OBJECT

    QString _key;
    QString _context;

    static LocaleHandler* _localeHandler;

public:
    LiveTranslator( const QString& context,
            const QString& key,
            QObject* target,
            const char* slot );

    static void setLocaleHandler( LocaleHandler* localeHandler );

private slots:
    void localeOrLanguageChangedHandler();

signals:
    void translate( const QString& string );
};

#endif /* LIVETRANSLATOR_HPP_ */

…and the body…

#include "LiveTranslator.hpp"

/* ---Initialize the locale handler pointer on app startup so we can tell if it
has been set properly later */
LocaleHandler* LiveTranslator::_localeHandler = 0;

/* ---Note that the target control is also used as the parent so the live translator dies when the control does */
LiveTranslator::LiveTranslator( const QString& context,
        const QString& key,
        QObject* target,
        const char* slot ) :  QObject( target ) {

    bool success;
    Q_UNUSED( success );

    // ---Save the context and key string
    this->_key = key;
    this->_context = context;

    // ---Die (during debug) if locale handler wasn't set properly before use
    Q_ASSERT( LiveTranslator::_localeHandler );

    // ---Watch for locale or language changes
    success = QObject::connect(
        LiveTranslator::_localeHandler,
        SIGNAL( localeOrLanguageChanged() ),
        SLOT( localeOrLanguageChangedHandler() )
    );
    Q_ASSERT( success );

    // ---Trigger specified slot when locale or language changes
    success = QObject::connect(
        this,
        SIGNAL( translate( const QString& ) ),
        target,
        slot
    );
    Q_ASSERT( success );
}

void LiveTranslator::localeOrLanguageChangedHandler() {
    /* ---Use the specified slot on the target to update the appropriate string
    attribute with the translated key */
    emit translate(
        QCoreApplication::translate(
            this->_context.toLocal8Bit().constData(),
            this->_key.toLocal8Bit().constData()
        )
    );
}

/* ---This function MUST be called once with a valid LocaleHandler before any
LiveTranslator classes are instantiated */
void LiveTranslator::setLocaleHandler( LocaleHandler* localeHandler ) {
    LiveTranslator::_localeHandler = localeHandler;
}

LiveTranslator encapsulates all the ugly stuff, including remembering the translation key, the intermediary slot/signal, and all signal/slot connections necessary for live translation. Using it is as simple as creating an instance of LiveTranslator, passing the constructor the translation key for the attribute (and the context, but more on that later), the target UI control, and the slot on that component that will accept the updated translation. Note that tr() only works with static key strings…

// ---This is valid
QString str1 = tr("string one");

// ---This is not!
Qstring str1 = "string one";
QString str2 = tr(str1);

…so rather than tr(), LiveTranslator must use QCoreApplication::translate() internally.

An example of LiveTranslator usage:

MyClass::MyClass() {
    Label* label = Label::create().text( tr("Label one") );
    new LiveTranslator( "MyClass", "Label one", label,
        SLOT(setText(const QString&)) );
    
    Option* option = Option::create().text( tr("Option one") )
        .description( tr("Option one description") );
    new LiveTranslator( "MyClass", "Option one", option,
        SLOT(setText(const QString&)));
    new LiveTranslator( "MyClass", "Option one description", option,
        SLOT(setDescription(const QString&)));
    
    ActionItem* actionItem = Option::create().title( tr("Action one") );
    new LiveTranslator( "MyClass", "Action one",
        actionItem, SLOT(setTitle(const QString&)));
}

Note that there is no need to save a reference to the new LiveTranslator instance since the constructor sets the “target” control as the parent. When the control is destroyed by your app the LiveTranslator will go with it. Also note that Momentics doesn’t know anything about LiveTranslator so you won’t be warned in the editor if the slot you provide is not valid. There is a Q_ASSERT in LiveTranslator though so you will know if the connection failed when testing your app in Debug mode.

The first parameter for the LiveTranslator constructor is the “context” where the translation key is located. When you use the tr() macro (or QML qsTr() function) the code parser makes note of where it found the key and stores it in the translation file along with the key. This way you could use the same translation key on different pages, and if the context is different you could have them translated differently. Again, the Index parser doesn’t know anything about the LiveTranslator class though so you have to tell it the context explicitly. For C++ the context is always the name of the class containing the code you are translating. Also, in case it isn’t obvious, the “key” parameter must be the same value as the one used with tr() on the line before it.

There is one thing you must do before using LiveTranslator in your C++ code and that is to give it a LocaleHandler to work with. Rather than force you to pass a LocaleHandler to every instance of LiveTranslator, you tell LiveTranslator which one to use just once with a static function call. If you created your app from one of the Momentics templates then you already have a handler you can use:

#include “LiveTranslator.hpp”

ApplicationUI::ApplicationUI() : QObject() {
    // prepare the localization
    m_pTranslator = new QTranslator( this );
    m_pLocaleHandler = new LocaleHandler( this );
    
    // Use this locale handler for all the live translations too
    LiveTranslator::setLocaleHandler( m_pLocaleHandler );

    ...
    ...
    ...
}

If all you care about live translating are normal UI components, then you have all you need now to easily implement live translation in your C++ code. If, however, you also want to have LiveTranslator work with certain system controls, such as SystemUiButton or SystemDialog you have a little more work ahead of you. The problem is that on these classes the attributes we would want to live translate are set with functions, not slots. This means that there is no way to tell LiveTranslator how to update the desired strings since it only works with Qt slots. The solution is quite simple though, all we need is to define a “helper” class for each system component type which can provide access to the setting function via slots. Here is a helper class for SystemUiButton

/*
 * SystemUIButtonTrxHlp.hpp
 *
 */

#ifndef SYSTEMUIBUTTONTRXHLP_HPP_
#define SYSTEMUIBUTTONTRXHLP_HPP_

#include <bb/system/SystemUiButton>

using namespace bb::system;

class SystemUiButtonTrxHlp: public QObject {
    Q_OBJECT

    SystemUiButton* _button;
public:
    SystemUiButtonTrxHlp( SystemUiButton* button );

public slots:
    void setLabel( const QString& label );
};

#endif /* SYSTEMUIBUTTONTRXHLP_HPP_ */

/*
 * SystemUiButtonTrxHlp.cpp
 *
*/

#include "SystemUiButtonTrxHlp.hpp"

SystemUiButtonTrxHlp::SystemUiButtonTrxHlp( SystemUiButton* button ) :
        QObject( button ) {
    this->_button = button;
}

void SystemUiButtonTrxHlp::setLabel( const QString& label ) {
    this->_button->setLabel( label );
       }

As with LiveTranslator the constructor automatically makes the target button the parent, so you can create an instance of the helper and forget it. Now a similar helper for SystemDialog

/*
 * SystemDialogTrxHlp.hpp
 *
*/

#ifndef SYSTEMDIALOGTRXHLP_HPP_
#define SYSTEMDIALOGTRXHLP_HPP_

#include <bb/system/SystemDialog>

using namespace bb::system;

class SystemDialogTrxHlp : public QObject {
    Q_OBJECT

    SystemDialog* _dialog;

public:
    SystemDialogTrxHlp( SystemDialog* dialog );

public slots:
    void setBody( const QString& text );
    void setRememberMeText( const QString& text );
    void setTitle( const QString& title );
};

#endif /* SYSTEMDIALOGTRXHLP_HPP_ */

/*
 * SystemDialogTrxHlp.cpp
 *
*/

#include "SystemDialogTrxHlp.hpp"

SystemDialogTrxHlp::SystemDialogTrxHlp( SystemDialog* dialog ) :
        QObject( dialog ) {
    this->_dialog = dialog;
}

void SystemDialogTrxHlp::setBody( const QString& title ) {
    this->_dialog->setBody( title );
}

void SystemDialogTrxHlp::setRememberMeText( const QString& text ) {
    this->_dialog->setRememberMeText( text );
}

void SystemDialogTrxHlp::setTitle( const QString& title ) {
    this->_dialog->setTitle( title );
}

Now let’s create a SystemDialog with live translation…

#include “SystemDialogTrxHlp.hpp”
#include “SystemUiButtonTrxHlp.hpp”

using namespace bb::system;

void MyClass::openDialog() {
    SystemDialog* dialog = new SystemDialog( tr(“Confirm Button”),
        tr(“Cancel Button”) );
    dialog->setTitle( tr(“Dialog Title”) );
    dialog->setBody( tr(“”Dialog Body”) );

    // ---Enable live translation
    new LiveTranslator( “MyClass”, “Dialog Title”, new SystemDialogTrxHlp( dialog ),
        SLOT(setTitle(const QString&)) );
    new LiveTranslator( “MyClass”, “Dialog Body”, new SystemDialogTrxHlp( dialog ),
        SLOT(setBody(const QString&)) );
    new LiveTranslator( “MyClass”, “Confirm Button”,
        new SystemUiButtonTrxHlp( dialog->confirmButton() ),
        SLOT(setLabel(const QString&)) );
    new LiveTranslator( “MyClass”, “Cancel Button”,
        new SystemUiButtonTrxHlp( dialog->cancelButton() ),
        SLOT(setLabel(const QString&)) );

    // ---Now for the magic ingredient
    bool success = QObject::connect( LiveTranslator::localeHandler(),
        SIGNAL(localeOrLanguageChanged()), dialog, SLOT(update()) );
    Q_UNUSED(success);
    Q_ASSERT(success);
}

Now for the bad news. See those few lines at the end of the function after the “magic ingredient” comment? Without them live translation of SystemDialog won’t work. Unlike regular UI components, SystemDialog and its brethren don’t automatically repaint themselves or their children when you change one of their attributes. Instead you make all the changes you want and then call or trigger the update() slot. The bad news? That function wasn’t added until the 10.2 API. This means that full live translation of system dialogs is not possible below that API level. I say “full” live translation because even with 10.0 and 10.1 if you leave SystemUiButton labels at their defaults or set them to one of the values that Cascades natively knows how to translate then those labels will be live translated with no additional effort by you. For instance, if you set the SystemDialog confirm button to read “Ok” or “Confirm” then Cascades will automatically add live translation of those words into any language you change your device to and you don’t even need to use the tr() macro when setting them. By carefully choosing your dialog title and body you might be able to get most of the way to live translation even with the lower API levels. Unfortunately, I don’t know of a list of button labels that Cascades can live translate natively so you might need a bit of trial and error. If there is any good news in this it’s that unlike regular screen elements, dialogs are transient entities so the lack of live translation is only evident for a moment. The next time your user opens a dialog, even the same one, it will be correctly translated into the new language.

I have provided the source files for LiveTranslator and some helper classes so all you need to do to get live translation working in your BlackBerry 10 Native C++ code is grab LiveTranslator from github and add it to your project (and any helpers you need too), add a call to LiveTranslator::setLocaleHandler() at the beginning of your main app class constructor and then call new LiveTranslator(…) with the appropriate parameters for each control attribute you want to be live translated. I leave it to the reader to create any additional helper classes they might need. I also created an example app that you can download and run without coding anything yourself. Note that this demo app includes code that live translates a SystemDialog so you must compile it with API 10.2 or better.

The Retranslate class makes it easy to perform live translations from QML, but Cascades does not provide a similar tool for C++ code. A few lightweight classes and extra lines of code bring similar functionality to C++, and best of all you don’t have to rewrite your app to take advantage of it. Just add one line of code for each UI component attribute you want to translate and the LiveTranslate class will take care of the rest.

Join the conversation

Show comments Hide comments
+ -
blog comments powered by Disqus