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

В этой статье я хотел бы рассказать о некоторых неудобствах, с которыми столкнулся при работе с системой переводов в 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) :
    QObject(parent)
{
    this->targetPropertyName = targetPropertyName;
    translation = t;
}

DynamicTranslator::DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName) :
    QObject(parent)
{
    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 (например, можно хранить все тексты в файлах, а в коде указывать только идентификаторы). Опять же, в больших приложениях пара десятков лишних строк (в случае с наследованием или написанием фабричной функции) не сделает погоды. Тем не менее, приведенные здесь решения могут сэкономить немного времени и строк кода, а также сделать этот код более естественным.

Критика и альтернативные решения всегда приветствуются — давайте писать более правильный, красивый и понятный код вместе. Благодарю за внимание.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 23

    +1
    Ради справедливости,
    Неудобство 1: хранение переводов
    проистекает от плохой архитектуры. Если команды представлять самостоятельными объектами, а не неявным набором глобальных хендлеров и отображений, то у вас будет красиво реализованный вывод документации на текущем языке.

    Если же вам реально нужен именно отложенный перевод, то да, обёрточка, которая явно показывает, что это строка, которая будет переведена на текущий язык при использовании. Не вижу причин расстраиваться из-за того, что ребята из Qt не написали её за вас.

    А вот
    Неудобство 2: динамический перевод при изменении языка
    реально присутствует. Лечится только ручным прописыванием того, что надо обновлять, в каком-нибудь обработчике changeEvent() — или в каждом виджете отдельно, или собирая эту информацию в глобальном мониторе вроде вашего DynamicTranslator.
      0
      Хотя, в общем-то, это одна и та же проблема: энергичность переводов.

      QObject::tr() и QCoreApplication::translate() выдают сразу QString, хотя по идее хотелось бы, чтобы они возвращали какой-нибудь QLanguageDependentString, который откладывает перевод до последнего момента и автомагически запоминает, кто запросил переведённую строку, чтобы в случае изменения языка отправить ему обновление.

      Но, мне кажется, это нереализуемо в текущих условиях, так как все хотят QString, а над ним обёртку не сделаешь.
        0
        Написать свой QLanguageDependentString, унаследовав его от QString:
        -при записи в него строки он переводит ее на текущий язык, и далее ведет себя как QString, содержащая перевод.
        -при всяком обращении проверяет, что сейчас установлен тот же язык, что и у имеющегося перевода. Если язык не совпал, переводим строку заново.

        А вообще да, у автора архитектурная проблема с расщеплением модель-представление, отсюда и костыль родился.
          0
          Это сработает только для самописных классов. Ну, можно еще исходники Qt под этот QLanguageDependentString переделать, но передавать придется по указателю, ибо, если я ничего не путаю, будет как-то так:
          Скрытый текст
          #include <iostream>
          
          class Base
          {
          public:
              virtual int method() { return 1; }
          };
          
          class Inherited : public Base
          {
          public:
              int method() { return 2; }
          };
          
          int main(int, char **)
          {
              Inherited *px = new Inherited;
              Base *py = px;
              std::cout << py->method(); //2
          
              //но
          
              Inherited x;
              Base y = x;
              std::cout << y.method(); //1
              return 0;
          }
          

            –1
            Зачем так-то? Нужно унаследоваться от QString и перезагрузить ему все методы своими. Не трогая исходники родной QString. Напоминаю, что наследование — это не способ избавления от повторного использования кода, а архитектурный прием, позволяющий объяснить классу, что мы передаем ему сущность, с которой он знаком и умеет работать.

            Внутри нашего класса мы будем, понятное дело, держать настоящий QString с переведенной строкой, которому и делегируем все обращения к своим методам.
              0
              Как тогда этот наш класс передать, скажем, в QWidget::setWindowTitle? Приведите, пожалуйста, пример.
              class QLanguageDependentString : public QString
              {
              //...
              };
              
              widget->setWindowTitle(QLanguageDependentString("Cool widget")); //Это не сработает по упомянутой выше причине
              
                0
                setWindowTitle(нашКласс.дайCюдаПереведеннуюСтроку())

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

                Да, при смене перевода придется перевыставить заголовки всем окнам. И вообще, перестроить весь интерфейс. А если язык выбрали арабский? Кошмар, там еще и раскладку элементов по форме быть может придется менять.

                Вы пытаетесь бороться с такой проблемой — вы получаете переводы, эти переводы куда-то сохраняете. В вашей модели появляются куски представления.

                После этого пользователь меняет язык. Это означает, на самом деле, что все что вы насохраняли надо уничтожить и перегенерировать заново. Либо выполнять перевод в последний момент — перед отдачей строк в интерфейс.

                Второе на самом деле означает, что придется всю работу со строками реализовывать в «ленивой» манере — нужно будет запоминать все действия, которые нужно сделать над строкой, а затем, когда надо отдать значения в интерфейс, эти действия проделывать.

                Гораздо проще при смене языка перестроить все, что от этого языка зависело.
                  0
                  setWindowTitle(нашКласс.дайCюдаПереведеннуюСтроку())

                  Так «нашКласс» это и есть мой Translation, разве нет? Хранит в себе строку, при необходимости возвращает в переведенном виде.
                  Гораздо проще при смене языка перестроить все, что от этого языка зависело.

                  Не всегда. Зачем мне, скажем, в приложении на 200 строк городить абстракций еще на 200 строк ради перевода пары кнопок? В больших энтерпрайз-проектах это, безусловно, оправдано, но не в маленьких утилитах.
                    0
                    Скажите, что вы будете делать, если у вас параметры в переводимой строке:

                    Копируется %2 файлов, сейчас копируется %1 файл

                    Copyng file %1 of %2

                    Этим в частности объясняется, почему механизм переводов сделан таким «неудобным», с вашей точки зрения.

                      0
                      Я делаю так: храню в Translator список строк-аргументов (positional arguments), при вызове метода, возвращающего переведенную строку, они подставляются простым QString::arg. Как-то так:
                      class Translation
                      {
                      //Тут то что уже было в статье
                      private:
                          QStringList arguments;
                      public:
                          void setArguments(const QStringList &list) { arguments = list; }
                          QString toString() const
                          {
                              QString s = QCoreApplication::translate(context.toUtf8().constData(), sourceText.toUtf8().constData(),
                                                          disambiguation.toUtf8().constData(), n);
                              foreach (const QString &a, arguments)
                                  s = s.arg(a);
                              return s;
                          }
                      };
                      
                      //Где-то в коде:
                      
                      for (int i = 0; i < 10; ++i) {
                          QWidget *w = new QWidget;
                          Translation t = Translation::translate("context", "Widget number #%1");
                          t.setArguments(QStringList() << QString::number(i + 1));
                          w->setWindowTitle(t);
                          new DynamicTranslator(w, "windowTitle", t);
                      }
                      

                      В Qt Linguist видим «Widget number #%1», переводим, к примеру, «Виджет номер #%1». Соответственно, будет 10 виджетов с номерами от 1 до 10, скажем, «Виджет номер #7».

                      Конечно, это не избавит от необходимости осуществить повторный вызов setWindowTitle вручную, если номер сменится, но это уже совсем другая история.
                        0
                        Ну вот, ленивый перевод в действии, о чем я и говорил — сам перевод и сборка строки происходят в тот момент, когда в них появляется необходимость.
        0
        Нет. Все формы Qt переводит сама по languageChange событию (если в формах прописаны алиасы и стоит галочка «translatable»). Руками в changeEvent нужно переводить только строки, которые вы сами динамически ставите в контролы или рисуете, т. е. то, что не в .ui-файлах.
          0
          Разве? Вроде UIC только генерирует соответствующий код для «своих» виджетов в методе retranslateUi(), а вызывать этот метод надо самостоятельно в переопредлённом обработчике changeEvent().
          0
          Согласен, что архитектура у меня не всегда соответствует общепринятым стандартам (тот же «модель-представление», упомянутый в комментарии выше). Однако, я глубоко убежден, что не стоит переусложнять то, что можно оставить простым без ущерба для разработчика и пользователя. Иначе говоря, KISS. Как показывает мой (хоть и небольшой пока что) опыт, добавить новый уровень абстракции, буде в нем возникнет необходимость, обычно можно без особых проблем, если, конечно, изначально руководствоваться здравым смыслом и немножко думать о будущем.
          +4
          Как-то у меня всё проще (хорошее ли это решение или нет — другой вопрос, просто делюсь своим вариантом. Просьба не минусовать). Linguist не использую, вместо него самописная утилита, которая складывает тексты в файл в формате alias=translation (можно и XML, конечно). Мой собственный наследник QTranslator его загружает и переопределяет виртуальный метод translate. Самое интересное — при смене языка в настройках я делаю

          QApplication::postEvent(QCoreApplication::instance(), new QEvent(QEvent::LanguageChange));

          Всё, весь текст, прописанный в .ui-формах алиасами переведен. Динамически формируемый текст нужно обновлять в обработчике LanguageChange.
            0
            А как ваша утилита обрабатывает множественные формы? Ну, то, что в Qt через %n. В английском языке для фразы «%n files were copied», например, потребуется два перевода, в русском — три.
              0
              Такая задача, скорее всего, тянет на отдельную диссертацию по компьютерной лингвистике (как и с универсальным интернациональным алгоритмом расстановки переносов).
            –2
            Никогда почему-то не задумывался о необходимости на лету во время выполнения приложения переключать языки. Ну, просто даже не как программист, а как пользователь.
            Для меня это все же пока остается «Установил приложение, запустил, выбрал язык в настройках, все»

            P.S. QTranslator активно использую, правда NOOP гораздо реже.
              +2
              Пример юзкейса — интерфейс на терминале в аэропорту. Язык будет переключаться регулярно и желательно делать это очень быстро.

              Другой вопрос, стоит ли делать терминал на Qt. Хотя как раз требование моментальной отзывчивости приложения и может послужить в пользу выбора Qt для этой цели.
                0
                Действительно, Вы правы.
              0
              Как то я тоже столкнулся с такими неудобствами, решением было написать свой простой движок на основе QHash.
              Я рассказывал о нем в этой статье.
              Получил плюсы: можно сохранять языки в свой формат файла, пользователи программы могут редактировать перевод без установки Qt Linguist.
                0
                Благодаря тому, что виджет w является родителем нашего DynamicTranslator, нет необходимости беспокоиться о его удалении — DynamicTranslator будет удалён вместе с QWidget.

                Строго говоря, с тем кодом, что приведен в статье, это не так, так как вы не передается parent из своего конструктора выше по иерархии.
                  0
                  Благодарю, поправил.

                Only users with full accounts can post comments. Log in, please.