вторник, 12 апреля 2011 г.

Делокализация Delphi приложения

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

Я датский практически не знаю. Для нормальной работы мне нужен английский язык. Комментарии не проблема - их можно перевести прямо в исходниках. А вот что делать с самими исходниками?. С одной стороны, все строки в исходниках должны быть написаны на английском. С другой стороны, для пользователя ничего не должно измениться. Интерфейс программы был на датском языке, значит должен оставаться на датском.

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

Далее речь пойдет о том, как все это было сделано.

Общая идея

Общий алгоритм работы выглядел так.
  1. Выдрать из исходных кодов все строки и сохранить их в отдельном текстовом файле.
  2. Перевести этот файл и получить перевод DK->ENG.
  3. Выполнить замену (swap) языков в исходных кодах на основе полученного перевода: в pas и dfm файлах все фразы на датском заменить на соответствующие фразы на английском.
  4. Создать реверсный файл перевода, т.е. из файла DK->ENG получить ENG->DK
  5. Локализовать приложение используя реверсный файл перевода.

Извлечение и перевод строк

В качестве основного рабочего инструмента я выбрал gettext. Библиотека проверенная, надежная и хорошо известная. Реализация gettext для Delphi существует, юникод в ней поддерживается. Кроме того, для gettext существует множество готовых утилит "на все случаи жизни".

Gettext умеет самостоятельно выдирать строки из исходных кодов. Вернее, автоматически он выдирает ресурсные строки и строки из GUI (dfm-файлов). Для того, чтобы он мог так же извлечь текстовые строки из PAS файлов, строки нужно заключить в gnugettext._(''):
using gnugettext;
   ...
   const s1: String = _('строка');
   ...
   ShowMessage(_('Ошибка!'));
После того, как все строки помечены, выдираем строки следующей командой:
dxgettext.exe -r -b PROJECT_DIRECTORY --delphi --nonascii
В результате gettext создает PO-файл, содержащий полный список строк. Вот пример PO-файла:
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2011-04-03 17:41\n"
"PO-Revision-Date: 2011-04-03 17:41\n"
"Last-Translator: Somebody \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: dxgettext 1.2.2\n"

#. Form4..Caption
#. FormPriceList..price..priceprice..EditFormat
#: PriceList.dfm:209
#: Unit1.dfm:6
msgid "Hyster administration"
msgstr ""

#: unit7.pas:179
#: users.pas:25
msgid "Mindst 8 karakterer langt"
msgstr ""
Думаю, идея понятна. В msgid содержатся оригинальные строки, в msgstr - перевод. Повторяющиеся строки в PO-файле сгруппированы, так что каждую строку придется переводить один и только один раз. Выполнить перевод PO файла можно вручную, а можно с помощью PO Editor.

Замена языка в исходных кодах

Итак, все строки выдраны и переведены. Следующая задача - поместить перевод в исходные коды. К сожалению, готовой утилиты для такой операции в арсенале gettext не нашлось. Ее пришлось написать - DxGetTextLangSwapper: скачать, просмотреть исходные коды на C#).

С помощью DxGetTextLangSwapper процесс замены языка в исходных кодах выполняется следующим образом:
DxGetTextLangSwapper PROJECT_DIRECTORY  SOURCE_PO_FILE TARGET_PO_FILE
Утилита перебирает все строки в исходном файле, находит в pas и dfm файлах места, где используется каждая строка, и заменяет строку ее переводом. Попутно, утилита генерирует реверсный PO-файл (если исходный файл содержит перевод строк с языка А на язык Б, то реверсный - с языка Б на язык А).

Утилита использует файл конфигурации DxGetTextLangSwapper.exe.config, позволяющий указать кодировки входных файлов:
<?xml version="1.0"?>
<configuration>
<!--  <startup>
    <supportedRuntime version="v2.0.50727" sku="Client"/>
  </startup>-->
  <appSettings>
   <!-- encoding of source PO file -->
    <add key="PO_FILE_ENCODING" value="65001"></add>
    <!-- encoding of PAS files -->
    <add key="PAS_FILES_ENCODING" value="1252"></add>
    <!-- encoding of DFM files-->
    <add key="DFM_FILES_ENCODING" value="1252"></add>
    <!--
        Entries in PO file and source files can be different during encoding problem.
        if BRUTOREPLACER turned on, then entry at specified text line is replaced always independent if actual value is equal to value from PO or isn't equal
        Program outputs warning about this replace with detailed info.    
    -->
    <add key="USE_BRUTOREPLACER" value="0" ></add>
  </appSettings>
</configuration>
Помимо кодировок, в файле есть параметр BRUTOREPLACER. Нужен он вот для чего. Dxgettext создает исходный файл в UTF8. Однако на практике некоторые строки могут попасть в PO файл в испорченном виде. Например, мне так и не удалось с помощью dxgettext вытащить датские строки в PO-файл корректно (ключ --nonascii не помог). Символы со всякими там умлаутами попортились. Человеку, для перевода, таких порченных строк достаточно, они вполне узнаваемы. Однако с подстановкой перевода будут проблемы.

Дело в том, что DxGetTextLangSwapper извлекает строки корректно (если правильно указать кодировку в настройках). В результате, в процессе замены, порченные строки из PO файла и корректные строки, извлеченные DxGetTextLangSwapper, не совпадут. В этом случае DxGetTextLangSwapper не выполнит замену и придется эти строки позже менять ручками.

Вот здесь и потребуется режим BRUTOREPLACER. Если указать в настройках BRUTOREPLACER=1, то DxGetTextLangSwapper выполнит замену даже в том случае, если строки не совпадают. Кстати, в этом режиме замена нескольких подряд идущих строк, находящихся в одной и той же строке файле, будет выполняться неправильно. Чтобы избежать проблем в каждой строке исходного файла должно быть не более одного вызова _('').

DxGetTextLangSwapper никогда не заменяет строки, если перевод строки отсутствует. Т.е. непереведенные строки в процессе работы игнорируются.

Замечу напоследок, что парсеры PO и DFM файлов написаны на скорую руку, так что некоторые особенности этих форматов вполне могут быть не учтены, что может привести к проблемам. DxGetTextLangSwapper сообщает обо всех подозрительных случаях.

Реверсный PO файл

Полученный реверсный PO файл, скорее всего, потребуется доработать. Дело в том, что исходный PO файл дает однозначное соответствие исходных строк и переводов: A->Б. А вот реверсный перевод Б->A запросто может быть неоднозначным. В этом случае PO-файл не скомпилируется. Неоднозначности нужно устранить (вручную).

Готовый PO файл нужно преобразовать в MO-формат (это умеет делать PO Editor). Как подключать MO-файл к приложению подробно расписано вот здесь, повторяться не буду.

Итоги

С помощью Gettext и DxGetTextLangSwapper мне успешно удалось де-локализовать один проект. Сейчас на подходе второй, так что обкатаю методику.

Утилита DxGetTextLangSwapper для замены языка в исходных кодах Delphi-проекта: скачать, просмотреть исходные коды на C#.

Update 23.04.2011: Успешно перевел второй проект. Правда, вылезло несколько ошибок в DxGetTextLangSwapper, связанных с парсингом формата PO. Поправил. Теперь не требуется совпадения количества строк "#." и "#:" перед msgid, и поддерживаются строки "#, fuzzy" (они просто игнорируются). Скачать версию 1.1.

Комментариев нет:

Отправить комментарий