Использование GNU gettext в Java
Немного основ. Как работает GNU gettext и чем он лучше традиционных способов локализации в Java таких как .properties
файлы.
- Во-первых нормальная поддержка plural form. В неанглийском языке нереально перевести какие-то сложные предложения с числительными. Единственный способ изобретать свои велосипеды при помощи ChoiceFormat. Выглядят такие переводы нереально дико.
- Во-вторых в исходном коде будут нормальные тексты, а не загадочные сокращения вроде
template.loginForm.okButtonLabel
.
К недостаткам можно отнести необходимость использовать сторонние утилиты, что не очень приветствуется в Java мире. К счастью, эти утилиты должны использоваться только на этапе разработки/сборки пакета. Они не нужны на продакшн-сервере или клиентской машине.
Итак, что это за утилиты и как их использовать. Процесс подготовки перевода можно разделить на несколько частей.
- Как-нибудь обозначить строки, подлежащие переводу в исходном коде. Традиционно функция, которая возвращает перевод строки называется просто символом подчеркивания
_
(это алиас полного названияgettext
). Другие вариантыngettext
(перевод фраз, зависящих от цифры),pgettext
(фразы, имеющие разный перевод в разном контексте) иnpgettext
(ngettext & pgettext в одном). Вызов этих функций является своеобразным маркером того, что строки, переданные в качестве аргументов, подлежат переводу. - Следующим шагом мы должны собрать все помеченные строки в одном
.pom
файле. Занимается этим утилитаxgettext
. - Далее этот файл используется чтоб создать
.po
файл с переводом на конкретный язык. Для этого используютсяmsginit
&msgmerge
. - И последнее, нужно откомпилировать
.po
файл с переводом в формат, который понимает наша программа. В мире C++/Python и других языков, которые используют нативные сишные библиотеки, компиляция происходит в.mo
файл специального бинарного формата. Для Java мы должны откомпилировать либо в.properties
файл, либо сразу в.class
. В.properties
смысла компилировать я не вижу, т.к. мы получим все те-же проблемы с невозможностью использования plural forms. В.class
переводы компилируются утилитойmsgfmt
.
Теперь рассмотрим все эти шаги более подробно
Маркировка строк для перевода
Про разные способы организации вызова фукнций, осуществляющих перевод, из кода можно почитать здесь.
Для доступа к переводам, используется стандартный ResourceBundle из поставки JDK.
Создание .pot
каталога строк, подлежащих перводу
Теперь мы должны найти все строки, нуждающиеся в переводе и сложить их в одном месте, так называемом словаре. Для этого существует утилита xgettext
. Она принимает на вход имена файлов в которых нужно искать строки и складывает их в заданном месте. xgettext
понимает большое кол-во языков, в том числе и Java.
Назовем наш словарь messages.pot
и положем в src/main/locale
.
find . -name "*.java" | xgettext -D . -f - -o - > src/main/locale/messages.pot
Теперь у нас в src/main/locale/messages.pot
будут храниться все строки, найденные в наших файлах с исходным кодом. Трогать этот файл не нужно. Его мы будем использовать для генерации .po
файлов, которые будем редактировать.
Создание .po
файла перевода под конкретный язык
Каждый язык имеет какие-то свои правила при склонениях разных слов и фраз. Например в русском это “1 обезьяна”, “2 обезьяны”, “5 обезьян”. GNU gettext знает все эти тонкости переводов и подготавливает вам файлик специальным образом чтоб мы могли правильно перевести все эти нюансы. Для работы с .po
есть две команды: msginit и msgmerge. Первая создает файлы, вторая их обновляет.
Создадим файл пеервода для русского языка
msginit -l ru -i src/main/locale/messages.pot -o src/main/locale/messages_ru.po
После того, как мы его создали, можем немного подправить заголовок. В частности, поскольку мы собираемся туда писать русские буквы, нужно content-type поменять с ascii на utf-8
"Content-Type: text/plain; charset=UTF-8\n"
msginit нужно выполнять только один раз, т.к. она перезапишет файл. В будующем, когда мы поменяем наши исходные коды и добавим/изменим строки перевода, нужно снова выполнить xgettext из прошлого пункта и в этот раз запустить msgmerge для обновления наших переводов. msgmerge добавит отсутствующие строки в .po
файл. Если же вы поменяли строку, то msgmerge пометит перевод как fuzzy и не будет его использовать. Вам нужно после обновления проверять все fuzzy строки и удалять слово fuzzy после проверки перевода чтоб все работало.
msgmerge -U --lang=ru src/main/locale/messages_ru.po src/main/locale/messages.pot
Компиляция .po
файла перевода в java-class
После подготовки переводов, мы должны откомпилировать их в ресурсы, доступные нашей программе во время выполнения. Если у вас maven проект, то при стандарной directory layout ресурсы хранятся в src/main/resources. Один из файлов перевода мы должны сделать используемым по умолчанию, иначе будет ошибка, что попытаться запустить программу в локале для которой отсутствует перевод. Предположим у нас два перевода для испанского и русского языков. И мы хотим русский использовать по умолчанию. Тогда мы должны выполнить следующие команды
msgfmt --java2 -d src/main/resources -r 'example.messages' src/main/locale/messages_ru.po # default
msgfmt --java2 -d src/main/resources -r 'example.messages' -l es src/main/locale/messages_es.po # spanish
Здесь мы назвали ресурс example.messages
. Это название важно далее мы будем использовать его в коде для доступа к этим ресурсам.
Возвращаясь к пункту про маркировку
И самое важно, как должен выглядеть Java-код в котором мы размечаем строки. Как нам рекомендует официальная документация по gettext for Java, мы можем сделать утилитный класс со статическими методами и использовать их чтоб получить перевод.
package example;
import gnu.gettext.GettextResource;
import java.util.Locale;
import java.util.ResourceBundle;
public class I18NUtils {
private static ResourceBundle resource =
ResourceBundle.getBundle("example.messages", Locale.getDefault());
public static String gettext(String text) {
return GettextResource.gettext(resource, text);
}
public static String _(String text) {
return gettext(text);
}
public static String ngettext(String msgid, String msgid_plural, long n) {
return GettextResource.ngettext(resource, msgid, msgid_plural, n);
}
}
Теперь когда нам требуется перевод мы можем делать
import static example.I18NUtils._;
System.out.println(_("Hello World!"));
Что очень похоже на вызов gettext из C/C++ или Python.
К сожалению, вызов ngettext не так похож и слегка многословен. Мы можем использовать ngettext в паре с MessageFormat для форматирования сообщений.
import static example.I18NUtils.ngettext
System.out.println(MessageFormat.format(
ngettext(
"Something random happened {0} time",
"Something random happened {0} times",
n
),
new Object[]{n}
));
Исходный код примеров можно скачать на github.
Что есть из готовой автоматизации
Есть готовый проект gettext-commons, который включает в себя интеграцию с Ant & Maven, что позволит упростить работу с gettext до вызова нескольких maven комманд. Можно ознакомиться с его туториалом для оценки чего он умеет.