понедельник, 8 июля 2013 г.

Перевод на лету: так ли всё просто, как кажется?

Сегодня мне хотелось бы рассказать о некоторых приёмах работы с системой перевода интерфейса в Qt. На первый взгляд может показаться, что всё предельно просто (тривиальные примеры в документации этому способствуют). Ну в самом деле, что сложного? Обёртываем строку в tr() или translate(), генернируем файл .ts, переводим, собираем в файл .qm, загружаем его — вот и всё. Но практика, как известно, зачастую несколько расходится с теорией.

Итак, переходим к примеру. Допустим, есть некое приложение, интерфейс которого должен быть переведён на пару-тройку языков. При этом к нему прилагается библиотека, в свою очередь также содержащая элементы, нуждающиеся в переводе. Итого: для каждого языка (кроме, возможно, исходного английского) имеется набор из трёх файлов переводов: appname_xx.qm, libname_xx.qm и (чтобы текст на кнопках подтверждения, отмены и т.д. также был на выбранном языке) qt_xx.qm (тут xx — имя локали для выбранного языка). Это ещё довольно простой случай, ведь иногда основной проект может зависеть от нескольких вспомогательных библиотек, и тогда файлов переводов может быть больше.

Уже посложнее, чем пример из документации, верно? А если мы, к примеру, хотим предоставить наиболее продвинутым пользователям возможность самим переводить интерфейс на их родной язык? В таком случае обычно предлагается помещать нужный файл переводов в соответствующую папку в домашнем каталоге. Итак, что мы имеем: несколько файлов переводов для разных частей приложения, расположенных в разных местах. Их обнаружение и загрузка — дело уже далеко не такое простое, как загрузка одного единственного файла перевода с известным путём.

Что же делать? Выход, конечно же, есть — создать класс, который брал бы на себя обязанности по обнаружению, загрузке и дальнейшему использованию переводов. Начнём, пожалуй:
 class Translator  
 {  
 private:  
   QString mfileName;  
   QLocale mlocale;  
   bool minstalled;  
   QList<QTranslator *> mtranslators;  
 public:  
   QString fileName() const  
   {  
     return mfileName;  
   }  
   QLocale locale() const  
   {  
     return mlocale;  
   }  
   bool isValid() const  
   {  
     return !mfileName.isEmpty();  
   }  
   bool isInstalled() const  
   {  
     return minstalled;  
   }  
   void install()  
   {  
     if (minstalled || !isValid())  
       return;  
     QStringList paths; //в эту переменную добавляем все пути к переводам (то есть список абсолютных путей к папкам)  
     foreach (QString path, paths)  
     {  
       QTranslator *t = new QTranslator;  
       if (t->load(mlocale, mfileName, "_", path, ".qm"))  
       {  
         mtranslators << t;  
         QCoreApplication::installTranslator(t);  
       }  
       else  
       {  
         delete t;  
       }  
     }  
     minstalled = true;  
   }  
   void remove()  
   {  
     if (!minstalled)  
       return;  
     foreach (QTranslator *t, mtranslators)  
     {  
       QCoreApplication::removeTranslator(t);  
       delete t;  
     }  
     mtranslators.clear();  
     minstalled = false;  
   }  
   void setFileName(QString fn)  
   {  
     bool wasInstalled = isInstalled();  
     if (wasInstalled)  
       remove();  
     mfileName = fn;  
     if (!isValid())  
       return;  
     if (wasInstalled)  
       install();  
   }  
   void setLocale(QLocale l)  
   {  
     bool wasInstalled = isInstalled();  
     if (wasInstalled)  
       remove();  
     mlocale = l;  
     if (wasInstalled)  
       install();  
   }  
 public:  
   explicit Translator(QString fn = QString(), QLocale l = QLocale())  
   {  
     minstalled = false;  
     setFileName(fn);  
     setLocale(l);  
   }  
   ~Translator()  
   {  
     remove();  
   }  
 };  
Что это мы тут нагородили? Давайте разбираться. Мы обернули список указателей на QTranslator в удобный для работы класс. Этот класс имеет методы для смены имени файла (например, это appname у файла appname_ru.qm или libname у файла libname_fr.qm, то есть, если можно так выразиться, "значащая" часть имени), а также для смены локали. И, конечно же, мы можем устанавливать и убирать экземпляры нашего класса. При этом внутри используется всё тот же знакомый по документации метод QCoraApplication::installTranslator. Данный класс почти готов к использованию, достаточно только указать, в каких папках искать переводы (метод install, переменная paths).

Уфф, половину проблемы решили. Осталось разобраться с получением списка языков, для которых доступны переводы. Приступим:
 QList<QLocale> availableLocales(QString fileName)  
 {  
   if (fileName.isEmpty())  
     return QList<QLocale>();  
   QList<QLocale> list;  
   QStringList validFiles;  
   QStringList paths; //тут вновь перечисляем пути к папкам, где могут лежать файлы переводов  
   foreach (QString path, paths)  
   {  
     QDir dir(path);  
     QStringList files = dir.entryList(QStringList() << "*.qm", QDir::Files); //получаем список файлов .qm  
     foreach (QString file, files)  
     {  
       //проверяем, что "значимая" часть имени файла соответствует имени, переданному в качестве параметра  
       if (file.left(fileName.length()) == fileName && !validFiles.contains(file))  
         validFiles << file;  
     }  
   }  
   foreach (QString file, validFiles)  
   {  
     int suffixPos = fileName.length() + 1; //индекс, с которого начинается "суффикс" имени файла, содержащий имя локали  
     int suffixLength = validFile.length() - fileName.length() - 4; //длина суффикса: вычитаем длину "значимой" части, длину "_" и длину ".qm"  
     QString localeName = validFile.mid(suffixPos, suffixLength);  
     QLocale l(localeName);  
     list << l;  
   }  
   return list;  
 }  
Вот такая простая, но полезная функция. Тут следует заметить, что при использовании её, а также приведенного выше класса предполагается, что файлы переводов именуются следующим образом: name_xx.qm, где name — "значимая" часть имени, соответствующая названию программы или библиотеки, xx — имя локали, а .qm - стандартный суффикс для файлов переводов, генерируемых средствами Qt.

Однако, пока мы решали проблему удобства, другая, не менее важная проблема — проблема производительности — осталась за кадром. А она имеет место быть. Как известно, перевод интерфейса "на лету" осуществляется путём переопределения метода QWidget::changeEvent, проверки типа события (он должен быть QEvent::LanguageChange) и, собственно, перевода видимых элементов с использованием функции tr. А теперь представьте себе программу, содержащую около тысячи видимых и подлежащих переводу элементов. Пользователь меняет язык интерфейса и все пять (к примеру) файлов перевода сначала убираются, а затем устанавливаются вновь (с другой локалью), вызывая аж десять событий смены языка. Итого — десять тысяч вызовов setText, setToolTip и т.д. Это уже будет заметно на глаз (и едва ли приятно, так как приложение на секунду "подвисает").

Надо что-то делать. Например, можно воспользоваться вот этим примером. Суть его такова: создаётся объект-посредник, к его слоту подсоединяется некий сигнал. При вызове слота запускается два таймера: "промежуточный" (в примере он назван "минимальным", что, на мой взгляд, не совсем корректно) и "максимальный". Если в течение некоторого промежутка времени слот вызывается снова, то "промежуточный" таймер перезапускается. Как только один из таймеров срабатывает, объект испускает соответствующий сигнал. Пример по ссылке выше приспособлен под определённую ситуацию, я же несколько "причесал" его под общий случай. Скачать можно тут.

А теперь то, ради чего было это лирическое отступление. Создадим ещё один простенький класс, который установим в качестве фильтра событий на экземпляр QApplication (или QCoreApplication). Вот он:
 class LanguageChangeNotifier : public QObject  
 {  
   Q_OBJECT  
 private:  
   SignalDelayProxy *proxy;  
 public:  
   explicit LanguageChangeNotifier()  
   {  
     proxy = new SignalDelayProxy(50, 100 this);  
     connect(proxy, SIGNAL(triggered()), this, SIGNAL(languageChanged()));  
   }  
 public:  
   bool eventFilter(QObject *o, QEvent *e)  
   {  
     if (e->type() != QEvent::LanguageChange)  
       return false;  
     proxy->trigger();  
     return true;  
   }  
 signals:  
   void languageChanged();  
 };  
Устанавливаем наш фильтр:
 LanguageChangeNotifier *notifier = new LanguageChangeNotifier;  
 QApplication::installEventFilter(notifier);  
Готово! Теперь в виджетах, где требуется перевод элементов, добавляем слот:
 void OurWidget::retranslateUi()  
 {  
   //тут переводим интерфейс, например так:  
   myLabel->setText(tr("some text"));  
 }  
И где-нибудь в конструкторе, ссылаясь на глобальный экземпляр LanguageChangeNotifier:
 connect(notifier, SIGNAL(languageChanged()), this, SLOT(retranslateUi()));  
Теперь мы получим крохотную задержку в одну десятую секунды (которую едва ли можно заметить) перед сменой языка интерфейса, зато избавимся от потенциально гораздо более существенной задержки, а также от зависания приложения из-за десяти тысяч лишних вызовов функций (от которых мы, к слову, тоже избавимся).

1 комментарий:

  1. Спасибо за статью. Отличный повод привести переводы в порядок.

    ОтветитьУдалить