habrahabr

Неудобства при работе с переводами в Qt и способы борьбы с ними

  • пятница, 2 января 2015 г. в 02:10:42
http://habrahabr.ru/post/247207/

В этой статье я хотел бы рассказать о некоторых неудобствах, с которыми столкнулся при работе с системой переводов в Qt, а также поделиться способами борьбы с этими неудобствами.

Для начала кратко напомню о том, как работает система переводов в Qt.

Прежде всего, разработчик при написании кода обёртывает строку, которая должна быть переведена на различные языки, в одну из специальных функций:

tr("Push me", "button text"); //Второй параметр - комментарий.
QCoreApplication::translate("console", "Enter a number"); //Первый параметр - контекст.

Далее в файле проекта указываются файлы, в которых переводчик будет осуществлять, собственно, сам перевод:

TRANSLATIONS += translations/myapp_ru.ts //Файлов может быть больше в зависимости от количества целевых языков.

Затем запускается утилита lupdate, создающая (или обновляющая) исходные файлы переводов (обычные XML-файлы), после чего переводчик может работать с ними при помощи специального инструмента — Qt Linguist. Строки, обёрнутые в функции tr и translate, будут обработаны утилитой и добавлены в .ts-файлы.

Наконец, когда все строки переведены, запускается утилита lrelease, превращающая исходные файлы переводов (.ts) в файлы .qm, имеющие специальный бинарный формат. Теперь остается только добавить в приложение следующий код:

QTranslator *t = new QTranslator;
t->load("/path/to/translations/myapp_ru.qm");
QApplication::installTranslator(t);

Всё, наши строки будут отображаться на нужном языке.

Неудобство 1: хранение переводов


Итак, хорошо, мы перевели строку, при запуске приложения загрузили файл перевода, в нашем текстовом поле (или где-то ещё) появился текст на нужном языке. Действительно, в таком тривиальном случае большего и не нужно. Однако, рассмотрим следующий пример. Допустим, у нас есть консольное приложение, где реализована обработка вводимых пользователем команд. Новые команды могут быть добавлены посредством установки функции-обработчика, например, вот так:

typedef bool (*HandlerFunction)(const QStringList &arguments);
QMap<QString, HandlerFunction> handlerMap;

void installHandler(const QString &command, HandlerFunction f)
{
    handlerMap.insert(command, f);
}

Всё отлично, но неплохо было бы при вводе, скажем «help command» выдавать справку по соответствующей команде command. Сделаем:

QMap<QString, QString> helpMap;

void installHelp(const QString &command, const QString &help)
{
    helpMap.insert(command, help);
}

Чувствуете подвох? Да, сначала всё будет хорошо:

installHelp("mycommand", tr("Does some cool stuff"));

Если QTranslator был установлен заранее, то мы получим переведенную строку. Но что, если пользователь решит сменить язык (иными словами, будет загружен другой файл переводов)? Строка останется прежней.

У этой проблемы есть несколько решений. Я приведу несколько, в том числе то, которое видится мне наиболее естественным и удобным.

Решение 1: фабрика

Можно заменить строку на функцию-фабрику, которая будет возвращать строку:

typedef QString(*HelpFactoryFunction)(void);
QMap<QString, HelpFactoryFunction> helpMap;

void installHelp(const QString &command, HelpFactoryFunction f)
{
    helpMap.insert(command, f);
}

Фабричная функция и её применение могут выглядеть следующим образом:

QString myHelpFactory()
{
    return tr("Does some cool stuff");
}

installHelp("mycommand", &myHelpFactory);

Решает ли это проблему? Да, перевод будет осуществляться каждый раз при вызове справки, таким образом, при смене языка справка будет показана переведенной на этот новый язык. Красивое ли это решение? Каждый считает по-своему, я же считаю, что нет.

Решение 2: QT_TRANSLATE_NOOP3

В заголовочном файле <QtGlobal> есть такой макрос — QT_TRANSLATE_NOOP3. Он помечает обёрнутую в него строку к переводу и возвращает анонимную структуру (struct), содержащую эту строку (в непереведённом виде), а также комментарий. В дальнейшем созданную структуру можно использовать в функциях tr и translate.

Надо ли говорить, что код получается громоздким и уродливым? Думаю, не надо. Кроме того, возникают сложности с передачей такой структуры в качестве параметра функции. Код:

typedef struct { const char *source; const char *comment; } TranslateNoop3;
QMap<QString, TranslateNoop3> helpMap;

void installHelp(const QString &command, const TranslateNoop3 &t)
{
    helpMap.insert(command, t);
}

Использование:

installHelp("mycommand", QT_TRANSLATE_NOOP3("context", "Does some cool stuff", "help"));

О том, что для перевода без комментария используется другой макрос (и другая структура) — QT_TRANSLATE_NOOP — я уж и вовсе молчу. А ведь пришлось бы городить перегрузку installHelp и превращать одну структуру в другую. Отвратительно. Оставим это на совести разработчиков Qt.

Решение 3: самописный класс-обертка

В каком-то смысле моё решение является усовершенствованным вариантом QT_TRANSLATE_NOOP3. Предлагаю сразу взглянуть на код:
translation.h
class Translation 
{
private:
    QString context;
    QString disambiguation;
    int n;
    QString sourceText;
public:
    explicit Translation();
    Translation(const Translation &other);
public:
    static Translation translate(const char *context, const char *sourceText, const char *disambiguation = 0, int n = -1);
public:
    QString translate() const;
public:
    Translation &operator =(const Translation &other);
    operator QString() const;
    operator QVariant() const;
public:
    friend QDataStream &operator <<(QDataStream &stream, const Translation &t);
    friend QDataStream &operator >>(QDataStream &stream, Translation &t);
};

Q_DECLARE_METATYPE(Translation)


translation.cpp
Translation::Translation()
{
    n = -1;
}

Translation::Translation(const Translation &other)
{
    *this = other;
}

Translation Translation::translate(const char *context, const char *sourceText, const char *disambiguation, int n)
{
    if (n < 0)
        n = -1;
    Translation t;
    t.context = context;
    t.sourceText = sourceText;
    t.disambiguation = disambiguation;
    t.n = n;
    return t;
}

QString Translation::translate() const
{
    return QCoreApplication::translate(context.toUtf8().constData(), sourceText.toUtf8().constData(),
                                                           disambiguation.toUtf8().constData(), n);
}

Translation &Translation::operator =(const Translation &other)
{
    context = other.context;
    sourceText = other.sourceText;
    disambiguation = other.disambiguation;
    n = other.n;
    return *this;
}

Translation::operator QString() const
{
    return translate();
}

Translation::operator QVariant() const
{
    return QVariant::fromValue(*this);
}

QDataStream &operator <<(QDataStream &stream, const Translation &t)
{
    QVariantMap m;
    m.insert("context", t.context);
    m.insert("source_text", t.sourceText);
    m.insert("disambiguation", t.disambiguation);
    m.insert("n", t.n);
    stream << m;
    return stream;
}

QDataStream &operator >>(QDataStream &stream, Translation &t)
{
    QVariantMap m;
    stream >> m;
    t.context = m.value("context").toString();
    t.sourceText = m.value("source_text").toString();
    t.disambiguation = m.value("disambiguation").toString();
    t.n = m.value("n", -1).toInt();
    return stream;
}


Я воспользовался интересным свойством lupdate: не важно, в каком пространстве имён находится функция translate, главное, чтобы она имела в точности такое имя, а также чтобы порядок аргументов и их тип были как в QCoreApplication::translate. В этом случае строки, обёрнутые в любую функцию translate, будут помечены к переводу и добавлены в .ts-файл.

Дальше дело остается за малым: реализуем свой статический метод translate так, чтобы он создавал экземпляр класса Translation, который по сути является более удобным аналогом анонимной структуры, которую возвращает QT_TRANSLATE_NOOP3. Добавляем также еще один метод translate, но уже не статический. Он просто вызывает внутри QCoreApplication::translate, передавая в качестве параметров контекст, исходную строку и комментарий, которые были указаны при вызове статического метода Translation::translate. Добавляем методы для копирования и (де)сериализации, и получаем удобный контейнер для хранения переводов. Не буду описывать остальные методы класса, так как они не имеют непосредственного отношения к решаемой задаче и тривиальны для разработчиков, знакомых с C++ и Qt, для которых и предназначена данная статья.

Вот как выглядел бы пример со справкой с использованием Translation:

QMap<QString, Translation> helpMap;

void installHelp(const QString &command, const Translation &help)
{
    helpMap.insert(command, help);
}

installHelp("mycommand", Translation::translate("context", "Do some cool stuff"));

Выглядит естественней, чем фабрика, и красивее, чем QT_TRANSLATE_NOOP3, не правда ли?

Неудобство 2: перевод без наследования


Второе неудобство, с которым я столкнулся в Qt — невозможность динамического перевода интерфейса без наследования хотя бы одного класса. Рассмотрим сразу пример:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QTranslator *t = new QTranslator;
    t->load("/path/to/translations/myapp_ru.qm");
    QApplication::installTranslator(t);
    QWidget *w = new QWidget;
    w->setWindowTitle(QApplication::translate("main", "Cool widget"));
    w->show();
    LanguageSettingsWidget *lw = new LanguageSettingsWidget;
    lw->show();
    int ret = app.exec();
    delete w;
    return ret;
}

Как видно из примера, мы загружаем файл перевода, создаём QWidget и устанавливаем его название. Но вдруг пользователь решил воспользоваться LanguageSettingsWidget и выбрал другой язык. Название QWidget должно поменяться, но для этого нам нужно предпринять какие-то дополнительные действия. Опять же, есть несколько вариантов.

Решение 1: наследование

Можно отнаследоваться от QWidget и переопределить один из виртуальных методов:

class MyWidget : public QWidget
{
protected:
    void changeEvent(QEvent *e)
    {
        if (e->type() != QEvent::LanguageChange)
            return;
        setWindowTitle(tr("Cool widget"));
    }
};

В таком случае при установке нового QTranslator будет вызван метод changeEvent, и, в нашем случае, setWindowTitle. Просто? Достаточно. Удобно? Я считаю, что не всегда (в частности, когда исключительно ради переводов приходится городить такой огород).

Решение 2: перевод извне

Также можно передать указатель на данный класс в другой класс, который уже унаследован от QWidget, и там вызывать соответствующий метод. Код приводить не буду — он очевиден и мало отличается от предыдущего примера. Скажу лишь, что это однозначно плохой способ — чем меньше классы знают друг о друге, тем лучше.

Решение 3: ещё один велосипед ещё одна обертка

Идея проста: а давайте мы воспользуемся таким удобным средством Qt, как мета-объектная система (подразумевается, что сигналы и слоты относятся сюда же). Напишем класс, которому будем передавать указатель на целевой объект, а также объект перевода из первой части статьи — Translator. Помимо этого укажем, в какое свойство (property) записывать перевод, или в какой слот передавать в качестве аргумента. Итак, меньше слов, больше дела:
dynamictranslator.h
class DynamicTranslator : public QObject
{
    Q_OBJECT
private:
    QByteArray targetPropertyName;
    QByteArray targetSlotName;
    Translation translation;
public:
    explicit DynamicTranslator(QObject *parent, const QByteArray &targetPropertyName, const Translation &t);
    explicit DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName);
protected:
    bool event(QEvent *e);
private:
    Q_DISABLE_COPY(DynamicTranslator)
};


dynamictranslator.cpp
DynamicTranslator::DynamicTranslator(QObject *parent, const QByteArray &targetPropertyName, const Translation &t)
{
    this->targetPropertyName = targetPropertyName;
    translation = t;
}

DynamicTranslator::DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName)
{
    this->targetSlotName = targetSlotName;
    translation = t;
}

bool DynamicTranslator::event(QEvent *e)
{
    if (e->type() != QEvent::LanguageChange)
        return false;
    QObject *target = parent();
    if (!target)
        return false;
    if (!targetPropertyName.isEmpty())
        target->setProperty(targetPropertyName.constData(), translation.translate());
    else if (!targetSlotName.isEmpty())
        QMetaObject::invokeMethod(target, targetSlotName.constData(), Q_ARG(QString, translation.translate()));
    return false;
}


Что же тут происходит? При создании экземпляра класса DynamicTranslator мы указываем целевой объект, перевод, а также имя слота (например, setWindowTitle) или имя свойства (windowTitle). Наш DynamicTranslator при каждой смене языка либо вызывает соответствующий слот при помощи QMetaObject, либо устанавливает нужное свойство при помощи setProperty. Вот как это выглядит на практике:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QTranslator *t = new QTranslator;
    t->load("/path/to/translations/myapp_ru.qm");
    QApplication::installTranslator(t);
    QWidget *w = new QWidget;
    Translation t = Translation::translate("main", "Cool widget");
    w->setWindowTitle(t);
    new DynamicTranslator(w, "windowTitle", t);
    w->show();
    LanguageSettingsWidget *lw = new LanguageSettingsWidget;
    lw->show();
    int ret = app.exec();
    delete w;
    return ret;
}

Благодаря тому, что виджет w является родителем нашего DynamicTranslator, нет необходимости беспокоиться о его удалении — DynamicTranslator будет удалён вместе с QWidget.

Вместо заключения


Безусловно, рассмотренные способы борьбы с неудобствами переводов не являются единственными, и уж тем более — единственно верными. Например, в достаточно больших приложениях вообще могут быть использованы сторонние средства перевода вместо тех, что предоставляет Qt (например, можно хранить все тексты в файлах, а в коде указывать только идентификаторы). Опять же, в больших приложениях пара десятков лишних строк (в случае с наследованием или написанием фабричной функции) не сделает погоды. Тем не менее, приведенные здесь решения могут сэкономить немного времени и строк кода, а также сделать этот код более естественным.

Критика и альтернативные решения всегда приветствуются — давайте писать более правильный, красивый и понятный код вместе. Благодарю за внимание.