Wiki

Забыли tr()?

by Jasmin Blanchette

QT’s tr() механизм для интернационализации очень легок в понимании, легок в использовании, и легок в программировании. Эта статья даст вам несколько советов, как правильно использовать tr(). Эта статья также рассказывает о QRegExp, XML, и Шведском языке.

Распространенные грубые ошибки
[BLUNDER:] Из Старо-Английского BLUNDEREN, "слепнуть", возможно из
Древнего Шведского BLUNDRA, "одноглазый", из Древнего Норвежского BLUNDA.
-- Американский словарь происхождений

Функция tr() двойная, по своей природе. Она является и lupdate маркером, и C++ функцией. Забудете о двойственности – получите ошибки. Некоторые ошибки будут результатом ошибок компилятора, остальные – lupdate, и, наконец, несколько ошибок пройдут незамеченными, и станут результатом сообщения в qt-bugs@trolltech.com. Давайте рассмотрим проблемы, которые могут возникнуть, и попробуем дать пути их решения.

  • Забыли о tr().
    Это достаточно легко – забыть закрыть видимые пользователю строки в tr(). Даже lupdate или C++ могут не поймать эту ошибку. Лишь QPainter ловит эту неточность, однако, как правило, это уже слишком поздно : "Cannot open %1" is unrecognizable as "Cannot open "C:\index.html".
  • Использование tr() в переменных.
    Аргументы в tr() должны быть строковыми литералами. Эта запись будет работать :
    QPushButton *ok = new QPushButton( tr("OK"), this );
    А вот эта – нет :
    QCString str = "OK";
    QPushButton *ok = new QPushButton( tr(str), this );

    Ясно, что строка «ОК» проходит через tr(), но lupdate пропустит второй пример, и также сделает ваш японский переводчик (это тот чувак, который будет переводить ваш текст с русского на японский, не зная толком ни того языка, ни другого... - прим. редактора :). Кстати «ОК» - допустим в большей части языков, но в японском он недопустим.
    Более сложная версия этой ошибки – затрагивает временные объекты:
    tr( QString("Cannot open %1").arg(fileName) )
    tr( "Cannot open " + fileName ) 

    Верный подход:
    tr( "Cannot open %1" ).arg( fileName )
  • Использование QT_TR_NOOP() без tr().
    QT_TR_NOOP() макрос и его длинная подруга QT_TRANSLATE_NOOP() редко используются, но в тех случаях, когда они используются – обычно их использование неверно. В отличие от tr() и translate(), эти макросы не производят перевода, а просто помечают строку для перевода. Результат перевода:
    const char *strings[2] = {
       QT_TR_NOOP("OK"),
        QT_TR_NOOP("Cancel")
    };
    for ( int i = 0; i < 2; i++ ) {
        QButton *but = new QPushButton( strings[i], this );
    }

    Дополнительная работа для вашего японского переводчика, и ложное чувство безопасности. Решение здесь – вложить strings[i] в вызов функции tr(). Другое решение – убрать этот макрос. Опытные программеры на языке Pascal имеют здесь преимущество: они знают, как НЕ использовать массивы строк.
  • Вызов tr() на объекте.
    Tr() - статический член-функция. Нормальный способ вызвать ее снаружи MyWidget - это MyWidget::tr(). Компилятор С++ позволит пройти myWidget->tr(), но Lupdate не достаточно умен, чтобы определить тип myWidget. В QT 3.1. lupdate выведет предупреждение в этом случае ("Cannot invoke tr() like this").
  • Забыли Q_OBJECT.
    Даже сейчас, кто-нибудь приходит ко мне в офис, и говорит : «Мужик, функция tr() не работает!». После этого я провожу полчаса в попытках найти и решить проблему, и в конце концов обнаруживаю, что tr() функция появляется в классе без макроса Q_OBJECT. Q_OBJECT необходим для сигналов, слотов, свойств, и tr(). Без этого – tr() использует имя базового класса, как контекст, в то время как lupdate все еще использует имя класса в запросе. Различные версии lupdate воспринимают это как ошибку, и выводят предупреждение : ("Class MyClass lacks Q_OBJECT macro").
  • Инсталляция QTranslator с опозданием.
    И даже сейчас, надоедливые люди, продолжают приходить ко мне в офис, и говорят : «tr() не работает». Я опять пытаюсь решить проблемы, для того чтобы обнаружить, что QTranslator был инсталлирован в qApp объекте, ПОСЛЕ того, как главное окно приложения было создано. Давайте быть осторожнее.
    Макрос, или виртуальная функция ?
    Убеждение, что tr() является макросом, или виртуальным членом функции QObject – очень распространено. Истина заключается в том, что tr() является статической фукнцией, она не виртуальна, и уж тем более не является макросом. Каждый класс с Q_OBJECT имеет свой tr() и trUtf8(), которые объявляются и реализуются автоматически.

    Вы можете подумать – почему tr() не реализована единожды и для всех объектов в QObject, использующих className() для обеспечения конекста. Причина такова: пусть А будет субклассом QObject, и пусть Б будет субклассом А. Код:
    setText( tr("Hello") );
    Строка "Hello" для lupdate находится в контексте "A". Когда этот код исполнен для Б, className() вернет Б, так что tr() будет ошибочно пытаться выполнить перевод в контексте “Б”

    Это очень важно, что все эти ошибки решаются ДО ТОГО, как .ts файлы посланы для перевода. Следующие секции продемонстрируют вам, как обнаружить и исправить tr() ошибки.

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

Первый инструмент для исправления ошибок #1, #2, and #3 - grep. Программисты под виндоуз, также могут попробовать использовать findstr, или закачать один из многих grep портов.

Следующая команда найдет строки, которые не окружены tr() (ошибка номер 1).
grep -n '"' *.cpp | grep -v 'tr('

Функция –n скажет grep`у печатать строку с информацией о номере. Функция –v инвертирует совпадения, так, что вышеследующее значит «найди все строки, которые совпадают», но не содержат вызовы tr(). Однако эта функция найдет, также, множество «невинных» строк, которые не требуют перевода.

Ошибка номер 2. Использование tr() в переменной – легко для обнаружения.
grep -n 'tr(' *.cpp | grep -v '"'
Это противоположность следующему : «найди все строки, содержащие tr(, но не содержащие “.”

Если приложение использует qApp-> translate(), используйте grep`ы, применяя translate вместо tr().

Ошибка 3. Неверное использование QT_TR_NOOP(), может быть легко предотвращено просмотром кода. Запустите grep, с параметром –l, для того, чтобы найти список файлов, которые используют QT_TR_NOOP() или QT_TRANSLATE_NOOP():
grep -l 'QT_TR' *.cpp
Для маленьких проектов использование grep будет более чем достаточным.

Псевдо-Шведский

Белбо приказал Абу изменить все слова, меняя каждую "a" на "akka" и каждую "o" на "ulla," из-за чего текст стал напоминать финский.
-- Umberto Eco

Самый применяемый метод обнаружения пропущенных вызовов tr() -- т.е. Ошибка #1 -- это запустить приложение с переводом. Но что, если его еще нет?

Одно из возможных решений данной проблемы принимает форму персонажа повара-шведа из Muppet Show. Идея в использовании программы, переводящей оригинальное англоязычное предложение в язык, на котором общался данный чувак (сие наречие мы будем называть псевдо-шведским).[1] Затем приложение включает псевдо-шведский язык, и любое английское слово, выглядящее непереведенным, вызвано неверным использованием tr().

Вот некоторые из английских фраз из приложений на Qt и их псевдо-шведские эквиваленты:

Английский Псевдо-шведский
&File &Feele-a
Big pink pig Beeg peenk peeg
Qt Quarterly Qt Qooerterly
Internationalization Interneshuneleezeshun
Do you want to save %1? Du yuoo vunt tu sefe-a %1? Bork Bork Bork!

Преимущество использования данного бреда в том, что результат выглядит очень странно, делая легким обнаружение обычных английских слов. Также приложение доступно для тестера, который разберет большинство фраз на псевдо-шведском.

Конечно, нет технических причин предпочтения шведского повара вместо немецкого хот-дожника или японского повара-сушиста. Любой язык, выглядящий по-импортному, может быть успешно использован для тестов.

Таблица показывает нужные замены символов:

an -> un      -f -> ff      th| -> t
au -> oo      -ir -> ur      -tion -> shun
a- -> e      -i -> ee or i      -u -> oo
en| -> ee      -ow -> oo      v -> f
-ew -> oo      |o -> oo      w -> v

Знак "|" используется для указания границ между словами, а "-" внутри слов. Таким образом, согласно правилу"-f", "f" заменяется на "ff" везде, кроме слов типа "fool" ("дурак" - прим. редактора), где "f" находится на границе слов. Программа lex, которая выполняет псевдо-шведизирование текста, доступна на www.almac.co.uk/chef/chef/ftp_chef.html.

Некоторые слова идентичны в английском и псевдо-шведском. Алгоритм может быть улучшен для искажения каждой "непереводимой" фразы, например, добавлением в конец тупизма типа "bork" (наверно, че-то типа нашего "бля" - прим.редактора) или другой ботвы.

Английский: File Edit Preferences Help
Псевдо-шведский: Feele-a Ideet Prefferences Help bork
Перевертыш: Elif Tide Secnereferp Pleh
Caps Lock: fILE eDIT pREFERENCES hELP
Смешанный шрифт: fIlE eDiT pReFeReNcEs hElP
Без гласных: Fl Dt Prfrncs Hlp
AltaVista (немецкий): Datei Bearbeiten Sie Prдferenzen Hilfe

Написание конвертора .ts

Как записать это в стандартном ASCII файле, остается загадкой.
-- Paul M. Roberts

Сейчас мы напишем программу, которая принимает непереведенный .ts файл и сгенерирует файл .ts на шведском языке. Эти файлы являются XML файлами, так что желательно привлечение Qt`s SAX, или DOM парсеров для их чтения. Вместо этого мы бы посоветовали использование QRegExp, класса, который не содержит секретов для ранних подписчиков Qt Quarterly. Задание относительно легкое – нам необходимо заменить следующие строки кода :

<source>&File</source>
<translation type="unfinished"></translation>

на

<source>&File</source>
<translation>&Feele-a</translation>
И сохранить результат.

А сама работа – чтение .ts файла, перевода, записи результата в другой .ts файл – очень проста. Всего 12 строк. Спасибо QFile и QRegExp:

    QRegExp rx( "(<source>(.*)</source>.*<translation)[^>]*>"
                "</translation>" );
    rx.setMinimal( TRUE );
 
    QFile in( fileName );
    if ( in.open(IO_ReadOnly) ) {
        QFile out( fileName + ".chef" );
        if ( out.open(IO_WriteOnly) ) {
            QString str = in.readAll();
            int pos = 0;
            while ( (pos = str.find(rx, pos)) != -1 ) {
                QString} newText = rx.cap(1) + ">" +
                        mock( rx.cap(2) ) + "</translation>";
                str.replace( pos, rx.matchedLength(),
                             newText );
                pos += newText.length();
            }
            out.writeBlock( str.utf8(), str.utf8().length() );
            out.close();
        }
        in.close();
    }

Функция mock() выполняет преобразование английских строк в Muck Svedeesh, Gnalskcab, cAPS lOCK, mIxEd CaSe, N Vwls, или AltaVista-Deutscher, как больше нравится.

Наследуем QTranslator

Я еще должен посмотреть на интересный кусок кода, пришедший от этих чуваков.
-- Alexander Stepanov

Совершенно иной подход, который не рекомендует использование QRegExp, или XML, является подклассом QTranslator. QTranslator::findMessage() функция является виртуальной, и может быть ПЕРЕреализована, как следующая :

QTranslatorMessage MockTranslator::findMessage(
        const char *context, const char *sourceText,
        const char *comment ) const
{
    return QTranslatorMessage( context, sourceText, comment, mock(sourceText) );
}

Как и прежде – mock() делает всю реальную работу.

При запуске приложения с инсталлированным MockTranslator, ошибки 1 и 3 – являются результатом НЕПЕРЕВЕДЕННОГО английского текста, в куче шведского.

Итак, мы не вовлекли в решение проблемы lupdate. Это справедливо, поскольку мы не знаем, возможно ли обмануть lupdate использованием tr() в переменной (ошибка 2). Это может быть протестировано следующим образом. Запустим lupdate для генерации .ts файла, и затем используем функцию «найти и заменить» текстового редактора, для замены каждого случая :

    <translation type="unfinished"></translation>

на
    <translation>Mock me!</translation>

Загрузим соответствующий .qm файл в подкласс QTranslator, который переопределяет findMessage() как:
QTranslatorMessage MockTranslator::findMessage(
        const char *context, const char *sourceText,
        const char *comment ) const
{
    QTranslatorMessage msg = QTranslator::findMessage(
            context, sourceText, comment );
    if ( msg.translation() == "Mock me!" )
        msg.setTranslation( mock(sourceText) );
    else
        qWarning( "Blunder #2 with %s, %s, %s", context,
                  sourceText, comment );
    return msg;
}

Мы считаем это лучшим способом ухватить ошибку 2.

Когда tr() не достаточно

Инструмент lupdate поддерживает исходные файлы С++ и файлы .ui для QT Designer. Некоторые приложения хранят видимые пользователю строки в других местах: базах, файлах конфигурации, скриптах, и тд. Вот несколько примеров, как эти файлы могут быть использованы для интеграции в круг lupdate, Qt Linguist, lrelease.

Наиболее простым путем является написание С++ программы, которая разжимает строки, откуда они пришли, и генерирует .ts файл. В свою очередь .ts файл может быть переведен, используя QT Lingiust, и конвертирован в .qm файл. QApplication поддерживает множество .qm файлов одновременно, так что не будет особой проблемы совмещать эту технику с другой. Недостаток этой техники заключается в том, что она проходит мимо совмещающего алгоритма lupdate.

Альтернатива – написать программу, которая будет генерировать ложный С++ код, который также может быть прочитан lupdate. Вот пример подобного кода :

    #if 0
    qApp->translate( "CustomerDB", "Agency" );
    qApp->translate( "CustomerDB", "Company" );
    qApp->translate( "CustomerDB", "Foreign" );
    qApp->translate( "CustomerDB", "International" );
    #endif

[1] Псевдо-шведский не похож на настоящий шведский, кроме случаев, если программа, которая это генерирует, не заполнена датским, норвежским или шведским...


Copyright © 2002 Trolltech. Trademarks