Thursday, February 18, 2010

Extending QTranslator

Although internationalization with Qt has very good tools to deal with translations inside an application, I was posed with the requirement to use existing files with proprietary format for the actual translated strings.

As a brief summary for people not yet used to the translation scheme provided by Qt, these are the steps a developer follows to make an application fully and easily translatable to any language:
  • surround all literal strings (the ones to be translated) with tr() functions, i.e. tr("This will be translated");
  • use lupdate command to extract translatable text into translation files (.ts)
  • translate the text files by means of Qt Linguist (since .ts files are actually XML files, they can even be edited with any text editor)
  • use lrelease command to transform the translated .ts files (text) into a binary representation format (.qm) files
Lots of developers do those steps regularly while developing internationalized applications using Qt, but what happens if you are requested to use localized strings provided from an existing central repository, and the .ts files aren't an option anymore?

Well, thanks to the very good design of the Qt framework, the QTranslator class can be extended in a quick and easy way just on one hand, use the proprietary file format to load the localized strings (thus not needing the lupdate / lrelease commands anymore) while on the other hand continue using the tr() functions as usual to mark what strings will be localized.

The fragment below is an example of the XML format used to hold the localized strings:
<translations locale="es_ES">
  <entry Id="save">Guardar</entry>
  <entry Id="save_as">Guardar como</entry>  
  <entry Id="open">Abrir</entry>
  <entry Id="exit">Salir</entry>
  <entry Id="error_network">Error de la red</entry>
</translations>
The idea was to derive QTranslator into a new class, and provide it with two distinct methods: one for loading and parsing the translation files (.trn) and the second one for translating the strings, this is searching the string id into the proper data structures (maps) and returning the translation, and this works fine since this method was declared virtual in the base class, QTranslator.

See Listing 1 and Listing 2 for the resulting source code after extending QTranslator class for the purposes described so far.

Happy localizations!

Listing 1 - MyTranslator class definition (.h)
#ifndef MYTRANSLATOR_H
#define MYTRANSLATOR_H

#include <QtGui>
#include <QTranslator>
#include <QMap>
#include <QString>

typedef QMap<QString, QString> LocaleMap;

class MyTranslator : public QTranslator
{
    Q_OBJECT

public:
    MyTranslator(QWidget *parent=0);
    ~MyTranslator();
    QString translate(const char *context, const char *sourceText, const char *comment = 0) const;
    bool loadFile(const QString &filename, const QString &locale, const QString &suffix = ".trn");

private:
    bool parseXml(const QString &filename, const QString &locale, const QString &suffix, LocaleMap &mapLocale);
    LocaleMap mapDefaultLocale;
    LocaleMap mapCurrentLocale;
    QString sCurrentLocale;
};

#endif // MYTRANSLATOR_H
Listing 2 - MyTranslator class implementation (.cpp)
#include "mytranslator.h"

MyTranslator::MyTranslator(QWidget *parent)
: QTranslator(parent)
{
    sCurrentLocale = "";    // No locale loaded so far
}


MyTranslator::~MyTranslator()
{

}


bool MyTranslator::loadFile(const QString &filename, const QString &locale, const QString &suffix)
{
    bool bLoadOk = false;

    if (sCurrentLocale != locale)
    {
        // While loading a new locale, clear the maps
        mapCurrentLocale.clear();
        mapDefaultLocale.clear();
    }

    // First of all, load default language into default locale map
    parseXml(filename, "en_US", suffix, mapDefaultLocale);

    // Parse the file again into current locale map
    bLoadOk = parseXml(filename, locale, suffix, mapCurrentLocale);

    if (bLoadOk)
    {
        // We did it, set the locale as loaded
        sCurrentLocale = locale;
    }
    
    return bLoadOk;
}


bool MyTranslator::parseXml(const QString &filename, const QString &locale, const QString &suffix, LocaleMap &mapLocale)
{
    QXmlStreamReader xml;
    QString realname = filename + locale + (suffix.isNull() ? QString::fromLatin1(".trn") : suffix);
    QFile file(realname);

    bool bError = !file.open(QFile::ReadOnly);

    if ( !bError )
    {
        xml.addData(file.readAll());

        QString key;
        while (!xml.atEnd())
        {
            xml.readNext();
            if (xml.isStartElement())
            {
                if (xml.name() == "translations")
                {
                    if (xml.attributes().value("locale").toString() != locale)
                    {
                        bError = true;
                        break;
                    }
                }
                else if (xml.name() == "entry")
                {
                    key = xml.attributes().value("Id").toString();
                    mapLocale[key] = xml.readElementText();
                }
            }
        }
    }
    else
    {
        qDebug() << "Failed to open file '" << file.fileName() << "':" << file.errorString();
    }

    return bError;
}


QString MyTranslator::translate(const char* /*context*/, const char* sourceText, const char* /*comment*/) const
{
    // We're not using context with this file format
    QString translation;

    // Search for source text in current locale
    translation = mapCurrentLocale[sourceText];
    if (translation.isEmpty())
    {
        // source text not found in current locale, let's try default one
        translation = mapDefaultLocale[sourceText];
        if (translation.isEmpty())
        {
            // no luck in default locale either
            // show source key if requested (mainly for debugging)
        #ifdef SHOW_SOURCE_TEXT
            translation = sourceText;
        #else
            translation = "";
        #endif
        }
    }
    return translation;
}

No comments:

Post a Comment