Данный пост является небольшим обзором GNU GetText. Часто если требуется поддержка нескольких языков в программе, люди не знают с чего начать. Я решил написать небольшое введение, которое позволит устранить данную проблему.
Интернационализация (internationalization сокращённо i18n) - способность программы поддерживать несколько языков.
Локализация (localization сокращённо l10n) - способность программы автоматически подстраиваться под языковые и локальные настройки системы.
GNU GetText является частью glibc, поэтому если сборка приложения происходит в Linux (или другой системе, использующей glibc), то линковщику не требуется указывать никаких дополнительных библиотек. В свою очередь GNU GetText также использует libintl (GNU Internationalization runtime library) и libiconv. Перевод выполняется в процессе выполнения программы на лету. Для обеспечения этого, все строки которые должны быть переведены в программе, оборачиваются в вызов функции gettext().
Во время выполнения программы, для перевода GetText должен знать локализацию системы, для этого в Linux системах традиционно используются переменные окружения:
LC_ALL,LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC, LC_TIME,LANG которые означают категории локалей (т.е. соответствующие части)
Чтобы посмотреть состояние этих переменных воспользуйтесь командой `locale`
Для работы GetText наиболее значим LC_MESSAGES, именно эта категория отвечает за перевод сообщений.
Данные переменные окружения хранят имена локалей, которые базируются на трех основных терминах: код языка, код страны и кодировка. Имена локализаций строятся из этих частей следующим образом:
кодязыка_КОДСТРАНЫ.кодировкаНапример коды языка и страны: en_US Английский - Соединенные Штаты
ru_RU Русский для России
zh_TW Традиционный китайский для Тайваня
В данных примерах отсутствует кодировка, также может отсутствовать и код страны. Т.е. например для русского языка в системе может быть несколько локалей, например: ru, ru_SU, ru_RU.KOI8-R, ru_RU.UTF-8 и т.д.
Расположение файлов перевода можно задать там где вам угодно, лишь бы их нашла программа, но традиционный путь в Linux: /usr/share/locale
Впрочем есть системы в которых путь к файлам локализации это /usr/local/share/locale или даже /usr/lib/locale, чтобы выяснить путь для вашей системы можно воспользоваться командой `locale -a -v`, вы увидите установленные локали и путь в котором они находятся.
При запуске программы в ней устанавливаются по умолчанию локаль С. Чтобы использовать другие локали, необходимо в программе использовать функцию setlocale. Имена категории локали совпадают с именами переменных окружения.
char *setlocale(int category, const char *locale);Параметр category может быть один из списка: LC_ALL - используется для всей локали.
LC_COLLATE - для соотнесения регулярных выражений (определяет значение интервалов и классов эквивалентности) и сопоставления строк.
LC_CTYPE - для соотнесения регулярных выражений, классификации символов, преобразований, сравнения (с учетом регистра) и работы функций широких символов.
LC_MESSAGES - для сообщений, подлежащих локализации.
LC_MONETARY - для форматирования денежных единиц.
LC_NUMERIC - для форматирования чисел (используется десятичный разделитель, разделители тысяч и т.п.).
LC_TIME - для форматирования календаря и дат.
Параметр locale должен содержать имя предпочитаемой локали, например ru_RU.KOI8-R, либо быть NULL. Если locale равен NULL то, поиск локали происходит в переменных окружения. Порядок зависит от реализации. В случае с glibc сначала (независимо от category) проверяется переменная окружения LANGUAGE, затем LC_ALL, затем переменная окружения с таким же названием, что и категория (LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC, LC_TIME), и в конце проверяется переменная окружения LANG. Используется первая существующая переменная окружения. Если ее значение не является правильным для локали, то локаль не изменяется, а setlocale возвращает NULL.
Успешный вызов setlocale() возвращает строку, соответствующую текущим установкам локали.
Чтобы gettext знал где находятся файлы перевода ему необходимо указать директорию с этими файлами, для этого используется функция bindtextdomain.
char * bindtextdomain (const char * domainname, const char * dirname);Первым аргументом domainname должно быть имя файла локализации называемого в терминах GNU GetText домёном. Имя файла должно быть не NULL и без расширения. Оно чаще всего совпадает с именем пакета.Вторым аргументом dirname должна быть директория в которой находятся иерархия файлов с локалями, или NULL чтобы узнать предыдущую заданную директорию.
На выходе в случае успеха возвращается установленная директория с локалями или NULL в случае неудачи.
Иерархия файлов внутри директории с локалями строится по следующей схеме:
locale_name/LC_category/domain_name.mo
locale_name - это имя локали, строящееся по тем же правилам что и в переменных окружения, т.е. по схеме кодязыка_КОДСТРАНЫ.кодировка. Причем как я упоминал выше, в имени локали может отсутсвовать код страны и кодировка. Применительно для GNU GetText LC_category всегда должен быть LC_MESSAGES, т.к. GetText ищет только директорию с переводимыми сообщениями. Имя домена это бинарный файл с переводимыми сообщениями.
Пример:
Пусть задана переменная окружения LANG=ru_RU.KOI8-R (locale_name)
Пусть файлы локализации хранятся в /usr/share/locale
Пусть задано имя приложения - gcc (domain_name)
Тогда поиск gcc.mo будет последовательно проходить по следующим путям:
/usr/share/locale/ru/LC_MESSAGES/gcc.mo
/usr/share/locale/ru_RU/LC_MESSAGES/gcc.mo
/usr/share/locale/ru_RU.KOI8-R/LC_MESSAGES/gcc.mo
Как видите вначале ищется языковая локаль, затем применительно к стране, и если не найдено в первых двух случаях, применительно к кодировке.
Как правило при создании дистрибутива как можно меньше стараются учитывать страну и тем более кодировку.
После того как известно имя локали и путь к локалям, приложению требуется имя файла в котором содержится перевод данного приложения (чаще всего совпадает исполняемым файлом приложения). Для указания конкретного файла переводов используется функция textdomain.
char * textdomain (const char * domainname);Входным параметром этой функции является имя домена. Выходным значением является заданный domainname или NULL в случае ошибки.
Более детальную справку по setlocale, bindtextdomain и textdomain можно посмотреть в соответствующем `man`.
При работе с GetText-совместимыми библиотеками интернационализации приходится иметь дело со следующими типами файлов:
*.pot текстовый файл (Portable Object Template) содержит строки полученные сканированием исходников с помощью утилиты xgettext, и ещё не содержит переводов.
*.po текстовый файл (Portable Object) содержит перевод.
*.mo бинарный файл (Machine Object) т.е. это скомпилированный файл переводов, который загружается конечным приложением – в нем и хранятся переводы для конкретной локали, поиск происходит по индексу от исходной строки.
Пример программы:
#include <locale.h>
#include <libintl.h>
#include <stdio.h>
#ifdef WIN32
#include <windows.h>
#endif
#define PACKAGE "example"
/* LOCALEDIR должен генерится системой сборки в зависимости от системы.
но для примера я использую директорию mo в той же директории что и приложение
т.е. в mo/ru/LC_MESSAGES
*/
#define LOCALEDIR "mo"
#define _(A) gettext(A)
#define N_(A) A
#define T_(A) _(A)
#define T_PLURAL(A,B,C) ngettext(A,B,C)
int main(int argc,char** argv)
{
/* Установка всей локали в соответствии
с переменными окружения
*/
setlocale(LC_ALL,"");
/* Установка пути к локалям
*/
bindtextdomain(PACKAGE,LOCALEDIR);
/* Установка имени файла перевода (домёна)
*/
textdomain(PACKAGE);
#ifdef WIN32
/* Для Windows укажем с помощью bind_textdomain_codeset
выходную кодировку переводов такую же как кодировка на консоли.
В Linux у меня никогда таких проблем не было.
Там версия gettext всегда улавливала кодировку терминала как минимум
потому что в LANG у меня всегда указывается кодировка.
*/
char windows_codepage[10];
snprintf(windows_codepage,10,"CP%d",GetConsoleCP());
bind_textdomain_codeset(PACKAGE,windows_codepage);
#endif
/* Отображаем 2 раза строку для перевода,
заметим что в pot попадет только одна строка.
*/
printf("%s\n",T_("This string will translate"));
printf("%s\n",T_("This string will translate"));
/* Пример использования форм множественного числа.
*/
int n;
for(n=1;n<10;++n)
/*TRANS: Данный комментарий видим переводчику.
Переводчик должен создать корректные формы множественного числа: */
printf(T_PLURAL("Give me %d apple\n","Give me %d apples\n",n),n);
return 0;
}Если не найден перевод, функция gettext возвращает исходную строку, иначе переведенную. Также мы используем функцию ngettext для перевода строк в которых есть слова, окончания которых должны меняться в зависимости от некоторого количества. (1 Штука, 2 Штуки, 5 Штук). Тут хочется отметить что обычно никто gettext напрямую не вызывает из соображений размера исходников. Для этого делаются подобные макросы:
#define _(A) gettext(A)
#define N_(A) A
#define T_(A) _(A)
Макрос N_ нужен для того чтобы создавать переменные которые позже будут переведены c помощью gettext. Например:
const char* str=N_("This string will translate");
printf("%s\n",_(str));
Сам GetText предлагает gettext_noop макрос аналогичный N_ (т.е. не навязывает вас делать свои макросы).
Создание перевода происходит в 3 этапа:
1) Парсинг исходных кодов программы с помощью утилиты xgettext и создание шаблона для перевода.
2) Копирование *.pot файла в рабочий *.po файл и перевод *.po файла.
3) Создание *.mo файла с помощью утилиты msgfmt, и его дальнейшее расположение в необходимой директории.
Перейдем в каталог с нашим примером и получим шаблон строк для перевода:
xgettext --default-domain=example --from-code=UTF-8 \
--add-comments=TRANS: \
--keyword=_ --keyword=T_ --keyword=N_ --keyword=T_PLURAL:1,2 \
-o example.pot example.cВ случае нахождения одинаковы строк для перевода, xgettext не будет повторять эту строку несколько раз в файле, а просто добавит ещё информацию о позиции в файле откуда вытаскивается строка. --add-coments=TRANS: позволяет добавить комментарии переводчику, начинающиеся с TRANS. Опции keyword говорят о том по каким ключевым словам вытаскивать фразы из исходника. Создадим файл рабочих переводов из шаблона:
msginit --input=example.pot --no-translator --locale=ru_RU --output-file=example.poПо сути данная операция просто копирует pot файл в po. Но я не пользую cp, потому что msginit за меня может заполнить информацию о переводчике, выставить корректно кодировку, а также указать правило для выборки форм множественного числа (plural-forms) в зависимости от указанной локали. Я использую --no-translator, потому что у меня не задана информация о email в моей системе, потом я правлю po файл руками. Заметим что в реальных приложениях эта операция делается один раз, и дальше существующий po файл растет по мере роста pot файла. Для обновления уже существующего po файла с переводами необходимо воспользоваться командой: `msgmerge existing.po new.pot –o existing.po` Дальше необходимо заполнить наш po файл:
# Russian translations for PACKAGE package
# Copyright (C) 2010 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Automatically generated, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-08-11 02:24+0700\n"
"PO-Revision-Date: 2010-08-11 01:21+0700\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: src/example.c:43 src/example.c:44
msgid "This string will translate"
msgstr "Эта строка для перевода"
#. TRANS: This comment is visible for translator
#. translator must create correct plural forms:
#: src/example.c:50
#, c-format
msgid "Give me %d apple\n"
msgid_plural "Give me %d apples\n"
msgstr[0] "Дай мне %d яблоко\n"
msgstr[1] "Дай мне %d яблока\n"
msgstr[2] "Дай мне %d яблок\n"В файле переводов первым идет MIME заголовок. В нем поля говорят сами за себя, там должны находится сведенья о переводчике и команде переводчиков. Пристальное внимание нужно уделить charset. Кодировка файла должна совпадать с этим полем. Рассмотрим формат переводимых строк:
white-space
# translator-comments
#. automatic-comments
#: reference...
#, flag...
msgid untranslated-string
msgstr translated-string
Все что помечено # является необязательным комментарием, и не используется при создании mo файлов. Комментарии которые после # имеют пробел создаются только переводчиком, остальные как правило с помощью различных утилит. Автоматические комментарии это комментарии от разработчиков. reference создается с помощью xgettext, в нем указывается имя исходника и номер строки из которой получена данная фраза. Что касается флагов после #, то единственный флаг на который стоит обратить внимание это #, fuzzy – пометка о том что данный перевод может быть не точным. Данный флаг может быть выставлен как переводчиком так и утилитой msgmerge при слиянии po и pot файлов. Ну и наконец самое главное это msgid – строка, которая вытащена из исходников и msgstr – это её перевод.
Стоит обратить внимание на Plural-Forms, данное правило добавлено msginit в соответсвии с указанной локалью ru_RU. Дело в том, что для большинства языков, число форм единственного и множественного числа является конечным. Для английского существует всего 2 формы - это единственное и множественное число, которое образуется добавлением –s или –es. Поэтому мы видим msgid форму единственного числа, msgid_plural – форму множественного числа. И этого достаточно, если учесть что все переводимые фразы в программе на английском языке. А вот перевод уже может содержать больше чем 2 формы. Поэтому msgstr заполняется как массив. Количество форм окончаний для русского языка – 3. Правило для выборки конкретной формы указывается в Plural-Forms. Хочу напомнить что его не нужно сочинять, msginit корректно его заполняет в зависимости от указанной локали.
Естественно вручную *.po файлы переводчикам править не нужно ;). Я рекомендую PoEdit: http://www.poedit.net/
Создадим *.mo файл для нашего *.po файла:
msgfmt -o example.mo example.poДальше наш mo файл необходимо положить в mo/ru/LC_MESSAGES. Собранное приложение example при запуске из той же директории, где находится директория mo - должно в зависимости от переменных окружения менять локализацию. Желающие могут поиграться с переменными окружения поменять их на ru или en и посмотреть как ведет себя приложение.
Архив с исходниками находится тут. Под linux как правило все библиотеки уже установлены. Для Windows, я использовал MinGW и для него придется скачать пакет GetText слинковать приложение с libintl. Можно просто указать соответствующие флаги при вызове make: mingw32-make "LDFLAGS += -lintl".
GetText поддерживается в Windows как портированное приложение, возникает вопрос что делать, чтобы не задавать локаль в переменных окружения? (Для оконных приложений это критично). Если покопаться в исходниках wxWidgets (данная библиотека совместима с GetText) можно увидеть следующий код:
LCID lcid = GetUserDefaultLCID(); //узнаем идентификатор локали
WORD langID = LANGIDFROMLCID(lcid); /*получаем идентификатор языка
из идентификатора локали (младшие 2 байта)*/
uint32 lang = PRIMARYLANGID(langID);//получаем идентификатор основного языка
uint32 sublang = SUBLANGID(langID); //получаем идентификатор региона/страныПо полученным идентификаторам составляется таблица в соответствии с MSDN для поиска по идентификатору имени страны и языка, которые можно подсунуть в setlocale,bindtextdomain. Если вам понравилось работать с GetText-подобными системами перевода приложений то можете попробовать себя в переводе свободных приложений ;). Для этого вам нужно посетить http://www-ru.gnu.net.ru/start.html и http://translationproject.org/team/ru.html
Хочется особо отметить, что для того чтобы ваш перевод попал в конечный дистрибутив вы должны заполнить и отправить Disclaimer на ваши переводы в FSF: http://translationproject.org/disclaim.txt
Ссылки:
http://www.gnu.org/software/gettext/http://www.gnu.org/software/gettext/manual/gettext.html
http://ru.wikipedia.org/wiki/Gettext
Комментариев нет:
Отправить комментарий