Посчастливилось мне как-то работать под руководством СТО, который по совместительству соавтор одного интересного проекта — 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.

Комментарии (5)


  1. BalinTomsk
    13.08.2015 23:18

    Для 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. tarasius
      14.08.2015 00:53
      +1

      Так тут тоже всё на лету. Менять язык в реалтайме — две команды:

        UseLanguage ('RU');
        Retranslatecomponent(self);
      
      Прозрачно более некуда и здесь — аж один юнит кода. Тут может его больше чем у Вас, но и возможностей больше. Те же домены, множественное число и т.д.
      И для перевода не нужно обьяснять переводчику где и как писать перевод, так как он может использовать любой редактор на свой вкус.


  1. soroktu
    14.08.2015 10:08

    Ссылка на исходники у меня не работает, гугл её почему-то отключил.


    1. tarasius
      14.08.2015 13:06

      Странно чем гуглу зип с исходниками не понравился. Исправил.


  1. pcmaniac
    03.09.2015 12:31

    А чем не устраивает родной переводчик, встроенный в среду? Ведь для локализации не достаточно заменить строки. Очень часто приходится менять размер и положение элементов на странице, а иногда и графику.