DxGetText — GNU Gettext for Delphi and C++ Builder

  • Tutorial
Посчастливилось мне как-то работать под руководством СТО, который по совместительству соавтор одного интересного проекта — GNU Gettext for Delphi and C++ Builder. Заценил я его только в Delphi, но этого достаточно чтоб понять принцип работы и разобрать какими фичами он обладает.
Вкратце это библиотека, позволяющая внедрять качественную локализацию в продукт общепринятым способом, работает так:
  1. пишем код, почти как обычно;
  2. запускаем приложение, сканирующее исходники на предмет текста, который нужно перевести;
  3. генерим РО файлы;
  4. переводим их в любом удобном редакторе;
  5. компилим РО файлы в МО файлы;
  6. на выбор либо внедряем перевод прямо в ЕХЕ либо кладём МО файлы рядом;
  7. наслаждаемся результатом — язык приложения можно менять даже без перезапуска.

Чем этот способ крут:
  • минимум изменений в коде приложения;
  • никаких DLL и сторонних компонентов, всё OpenSource;
  • РО файлы — достаточно распространенный инструмент перевода, что значит перевод можно даже отдать на аутсорс, и переводчик знает что с этим делать;
  • перевод всего — формы, фреймы, месседжбоксы, и всё что угодно;
  • корректный перевод слов в множественном числе в любом языке;
  • полная поддержка Unicode.

Итак, приступим к установке.
Сайт и исходники давненько не обновлялись. Можно взять скомпилированные тулзы здесь, а можно взять исходники здесь и собрать самому. Это инструменты собственно для генерации PO файлов и действий с ними. А для проекта Delphi нам нужно всего лишь добавить в uses один файл — gnugettext.pas.

Теперь попробуем использовать.
При запуске приложения нам неизвестен его язык, т.е. он будет “Untranslated”. Указать язык можно где угодно — в initialization, в конструкторе формы, после выбора юзером языка и т.д. Для того, чтоб перевелась форма, в ее конструкторе следует вызвать метод translatecomponent.
Попробуем создать VCL Form Application с таким текстом в конструкторе формы:
procedure TForm1.FormCreate(Sender: TObject);
begin
  UseLanguage ('EN');
  translatecomponent(self);
end;

Список поддерживаемых кодов языка можно посмотреть тут.
Далее создадим кнопку с Caption:=’Translate me’, по нажатию на которую покажем месседж и переключим язык приложения:
procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(_('Changing language'));
  UseLanguage ('RU');
  Retranslatecomponent(self);
end;

Функция “_” (или “gettext”) попробует найти перевод указанного текста и вернёт его, если найден, или же вернёт текст со входа.
Сейчас приложение не имеет локализации, РО и МО файлов нет. Всё работает, никаких ошибок, просто без перевода. Попробуем это исправить.
В папке приложения создадим папку с именем “locale”, в ней папки с языками, в нашем случае “ru” и “en”, и в каждй папке языка создадим папку “LC_MESSAGES”, в которой создадим пустой текстовый файл “default.po”. После этого в корневой папке приложения создадим файлик updatepofiles.cmd с таким содержимым:
echo Extracting texts from source code

dxgettext -q --delphi --useignorepo -b .

echo Updating Russian translations
pushd locale\ru\LC_MESSAGES
copy default.po default-backup.po
ren default.po default-old.po
echo Merging
msgmergedx default-old.po ..\..\..\default.po -o default.po
del default-old.po
popd

echo Updating English translations
pushd locale\en\LC_MESSAGES
copy default.po default-backup.po
ren default.po default-old.po
echo Merging
msgmergedx default-old.po ..\..\..\default.po -o default.po
del default-old.po
popd

Он будет генерить РО файлы с нашего исходника тулзой dxgettext.exe в файл default.po в корневой папке приложения. Так как при генерации создается файл без переводов, то нужно учесть, что при добавлении туда переводов при следующем вызове dxgettext они исчезнут. Для этого скрипт далее делает слияние (msgmergedx) нового файла без переводов со старым файлом с переводами. В итоге мы сохраним все старые записи и переводы, а новые записи добавятся без перевода. Нужно учесть что все одинаковые тексты в оригинале будут сгруппированы в одну запись и переведены одинаково. То есть если у нас на трёх формах будет пункт меню “Save”, то в РО файле будет только одна запись и перевод будет одинаковым для всех трёх форм. Полезная фича dxgettext в том, что в комментарии для записи будут перечислены все места в коде, где она встречается.
Теперь попробуем что-то перевести. Можно использовать любой редактор из просторов интернета, можно просто в текстовом редакторе, но я взял редактор от того самого автора, моего бывшего босса — Gorm. Он точно также с открытыми исходниками и насыщен фичами больше некуда. Итак, идём в папку locale\en\LC_MESSAGES\ и открываем Gorm’ом default.po.
Для корректной работы перевода нужно указать какой язык этого файла, и при желании прочую инфу — автор перевода, версия, и т.д. Для этого открываем File / Edit header и в поле Language выбираем English. Можно заметить, что в перевод попал и ненужный нам пункт с именем шрифта. Чтоб его не переводить можно нажать кнопку Ignore справа. А для остальных впишем перевод в поле Translation внизу:

Сохраним. То же сделаем и для русского языка, указав его в хэдере. Теперь чтоб перевод появился в нашем приложении, РО файл нужно скомпилировать. Если был установлен dxgettext, это можно сделать двойным кликом по РО файлу, можно прямо в Gorm’е — Tools / Compile to MO file. Для того, чтобы наше приложение увидело файлы перевода, нужно у проекта поменять Output directory на корневую папку проекта (пустой путь или “.”). Теперь запустим его.


Кроме того, можно избавиться от МО файлов, и приложение будет работать с переводами без них. Для этого создадим еще один скриптик в корневой папке приложенияи назовём его translate.cmd. Он нам скомпилит РО файлы (да, еще один способ это сделать) и внедрит переводы в ЕХЕ файл.

set sourceroot=%CD%\

echo Compiling language files and embedding those
echo English...
cd %sourceroot%locale\en\LC_MESSAGES
msgfmt.exe -o default.mo default.po
echo Russian...
cd ..\..\ru\LC_MESSAGES
msgfmt.exe -o default.mo default.po
cd ..\..\..

echo Embedding translations...
copy Project1.exe Project1_Translated.exe
assemble.exe --dxgettext Project1_Translated.exe

echo Compiling language files and embedding those completed

cd %sourceroot%
pause

Теперь у нас есть файл Project1_Translated.exe, который можно перемещать в другую папку, на другую машину, и переводы в нём уже встроены.

Далее рассмотрим типичные задачи, с которыми столкнётся программист, внедряя локализацию в приложение.

Строка в формате

Иногда нужно в текст втсавить значение переменной. Для перевода такой строки не нужно ее разбивать на куски, ведь тогда теряется смысл и переводчику будет трудно понять о чём этот кусок. Перевод можно сделать таким образом:
var s:string;
    d:integer;
begin
  s:=_('Apple');
  d:=7;
  showmessage(format(_('%s count: %d'),[s,d]));
end;

Функция перевода должна быть внутри формата, тогда перевод на русский к примеру будет “Количество %s: %d”. При этом Gorm будет показывать предупреждение, если количество параметров формата в переводе отличается.

Множественное число

В разных языках множественное число может переводиться по-разному в зависимости от количества считаемых предметов. DxGetText может справиться с этой задачей с помощью функции ngettext. Ей нужно передать слово в единственном числе, во множественном, и количество для которого нам нужен перевод. Пример:
var i:integer;
    s:string;
begin
  s:=emptystr;
  for i:=1 to 11 do
    s:=s+format(('%d %s'),[i, ngettext('apple', 'apples', i)])+slinebreak;
  i:=21;
  s:=s+format(('%d %s'),[i, ngettext('apple', 'apples', i)]);
  showmessage(s);
end;


Тут проявляется первый минус Gorm’а — такие переводы он не умеет редактировать. Поэтому вызовем updatepofiles.cmd для нашего проекта и попробуем написать перевод в текстовом редакторе. Для этого откроем locale\ru\LC_MESSAGES\default.po и найдём наши яблоки. Перевод будет вот таким:
msgid «apple»
msgid_plural «apples»
msgstr[0] «яблоко»
msgstr[1] «яблока»
msgstr[2] «яблок»

Скомпилим РО файл и запустим приложение. Мэсседж с яблоками будет выглядить так:
English Русский
1 apple 1 яблоко
2 apples 2 яблока
3 apples 3 яблока
4 apples 4 яблока
5 apples 5 яблок
6 apples 6 яблок
7 apples 7 яблок
8 apples 8 яблок
9 apples 9 яблок
10 apples 10 яблок
11 apples 11 яблок
21 apples 21 яблоко

Одинаковые слова с разными значениями

Если в приложении встречаются слова, которые при разном значении в языке по умолчанию пишутся одинаково (омонимы), то dxgettext.еxe, который вытаскивает переводы в РО файл, посчитает это одним и тем же словом, и перевод соответственно будет одинаков во всех местах, где встречается это слово. К примеру, если у нас чудным образом в приложении нужно перевести слово “bow” как лук (который стреляет), и как смычок для музыкальных инструментов, то получится неопределенность. Пример:
var s:string;
begin
  s:=_('bow');
  Showmessage(
    format(_('Use %s to shoot enemies.'),[s]) + slinebreak +
    format(_('Use %s to play violin.'),[s]));
end;

Тут уже либо стрелять смычком, либо играть луком. Обойти такую проблему можно разделив слово по значениям.
var s1,s2:string;
begin
  s1:=_('bow_music');
  s2:=_('bow_weapon');
  Showmessage(
    format(_('Use %s to shoot enemies.'),[s2]) + slinebreak +
    format(_('Use %s to play violin.'),[s1]));
end;


Результат:
Используй лук для стрельбы по врагам.
Используй смычок для игры на скрипке.

Домены

Иногда приложение разделено на модули, и эти модули нужно переводить отдельно. Для этого в dxgettext используются домены. По умолчанию все переводы попадают в домен “default”, потому и РО файл так называется. Но если мы будем использовать функции dgettext или dngettext где домен указывается параметром, то перевод будет попадать в РО файл с указанным доменом и соответственно поиск перевода будет выполняться в файле с именем домена. Кроме того, домен можно установить перманентно процедурой textdomain.

Выводы


Функционала dxgettext хватает с головой для локализации даже профессиональных и больших программных продуктов. Работает шустро, при внедрении переводов в исполняемый файл добавляет несколько мегабайт к его объёму, что сносно в случае Delphi, где небольшое приложение и так уже весит несколько десятков мегабайт ;)

P.S: Исходники рассмотренного примера можно скачать тут или на GitHub.
Поделиться публикацией

Комментарии 10

    0
    Для Borland Builder в ряде продуктов — я делал следуюший функционал — при нажатии на скрытую кнопку — генерился/aпдейтилса XML файл — который хранил в себе структуру и отношения всех контролов приложения — формально это несколько строчек кода.

    Переводчики добавлют ноды языка в простом редакторе

    <?xml version=«1.0»?>
    <Lаbel Name=«English»>
    <Trаnslate Name=«Russian»><![CDATA[Метка]]></Trаnslate>
    <Trаnslate Name=«French»><![CDATA[Etiquette]]></Trаnslate>
    </Lаbel>


    При выборе языка из меню — форма мгновенно апдейтится на выбранный язык. Каждый tag контрола хранит адрес нода xml и на refresh/create выбирает нужный язык. Все на лету — все прозрачно — никаких библиотек.
      +1
      Так тут тоже всё на лету. Менять язык в реалтайме — две команды:
        UseLanguage ('RU');
        Retranslatecomponent(self);
      
      Прозрачно более некуда и здесь — аж один юнит кода. Тут может его больше чем у Вас, но и возможностей больше. Те же домены, множественное число и т.д.
      И для перевода не нужно обьяснять переводчику где и как писать перевод, так как он может использовать любой редактор на свой вкус.
      0
      Ссылка на исходники у меня не работает, гугл её почему-то отключил.
        0
        Странно чем гуглу зип с исходниками не понравился. Исправил.
        0
        А чем не устраивает родной переводчик, встроенный в среду? Ведь для локализации не достаточно заменить строки. Очень часто приходится менять размер и положение элементов на странице, а иногда и графику.
          0
          1. Не переводит строковые литералы в файлах .pas?
          2. В принципе параметром можно было бы прицепить размеры и стиль шрифта. Если перевод сильно длиннее оригинала, то выводить меньшим по размеру шрифтом.
          3. В скриптике updatepofiles.cmd надо бы добавитьсохранение default.po в папке locale. Или я что-то не так делаю?

          4. Перевод надо запускать по сыбытию OnShow главной формы, а не OnCreate.
          Тогда можно перебором всех форм всех их перевести.
          Как-то так для Дельфи

          //**************************************************
          procedure LangIni();
          var
            i: integer;
          begin
          for i := 0 to Application.ComponentCount - 1 do
            begin
              if (Application.Components[i] is TForm)
              then translatecomponent(application.Components[i]);;
            end;
          end;
          
          
          //**************************************************
          procedure LangChange();
          var
            i: integer;
          
          begin
          for i := 0 to Application.ComponentCount - 1 do
            begin
              if (Application.Components[i] is TForm)
              then Retranslatecomponent(application.Components[i]);;
            end;
          end;
          
          procedure TForm0.FormShow(Sender: TObject);
          var
            i: integer;
          begin
           if RadioGroup1.ItemIndex=0 then
           begin
               LangC:='EN';
            UseLanguage ('EN');
           end
            else
            begin
               LangC:='RU';
            UseLanguage ('RU');
            end;
           LangIni();
           end;
          
          
            0
            1. Что Вы имеете в виду? Перевод делается для DFM и вызовом функций, например
            s:=_('Apple');
            

            То есть всюду, где в исходниках присваивается строка, приходится вызывать эти функции.
            2. Перевод не может менять шрифт, это всего лишь перевод, текст на входе — текст на выходе. Как выводить переведенный текст должно быть реализовано уже в Вашем приложении, потому как это уж очень кастомизировано у каждого.
            3. Вы правы.
            4. Переводить на OnShow избыточно если для измения языка нужно перезапускать приложение. А если менять на лету, тогда да. И то, код для перевода можно поместить в событие изменения языка, а не в OnShow формы. Если оставить в OnShow, то можно сравнивать поменялся ли язык и вызывать перевод только если язык формы отличается от текущего языка приложения.

            Если форм много, и нужно всюду добавить одинаковый код для перевода, сделайте родительскую форму, поместите общий код туда, а все формы, показываемые юзеру унаследуйте от неё.
              0
              1. Да, надо прочитать мануал еще раз )
              Но по крайней мере в gorme данные из строк подобных (s:=('Apple'); ) не показывались. И соответственно не переводились.
              2. Перевод не может менять шрифт, это всего лишь перевод, текст на входе — текст на выходе. Как выводить переведенный текст должно быть реализовано уже в Вашем приложении, потому как это уж очень кастомизировано у каждого

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

              4. Там же все равно надо первым вызывать Translate… а при смене языка Retranslate…
              В моем случае программа (со всеми формами) уже написана и надо быстренько «прикрутить» туда мультиязычность. Менять структуру программы особой возможности нет.

                0
                Но по крайней мере в gorme данные из строк подобных (s:=('Apple'); ) не показывались. И соответственно не переводились.

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

                А как насчет родительской формы? Просто ко всем формам добавить (заменить TForm на своего) родителя, и уже OnShow писать на родительской форме. Не нужно будет прописывать событие у каждой формы, да и изменение небольшое в структуре.
                Только тогда если уже где-то был OnShow, не забыть добавить inherited.
            0
            >Нужно вызывать функцию для перевода с литералом как параметр.

            1. мм… да заменил все строки типа s:='Apple'; на s:=_('Apple'); в файле po появились соотв. строки.
            А автоматизации никакой для этого нет? В пакете Дельфи конечно есть GrepSearch, но все равно найденные строки пришлось обрабатывать вручную.

            2. Так я не добавляю в OnShow каждой формы перевод, только в OnSHow главной формы перебираю в цикле все формы. Это происходит один раз при запуске программы. Ну и в функции переключении языка в runtime.

            ps. программа конечно отличная и Вам спасибо за данный пост! В течение 1 дня фактически прикрутил перевод к существующему проекту, причем большую часть времени занимался именно переводом ))

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

            Самое читаемое