Простой пример

Простой пример [Вперед] [Вверх] [Назад] [Содержание]
Дальше: Совместное использование Вверх: Интернационализация сообщений Назад: Интернационализация сообщений

Простой пример

Сначала мы напишем совсем крохотную программу, а затем снабдим ее механизмами интернационализации. Ограничимся двумя языками, но немного усложним себе задачу тем, что сделаем нашу программу псевдо-интерактивной. Пусть программа будет выдавать в бесконечном цикле какое-либо сообщение с учетом установок locale, сделанных через переменные окружения перед запуском, а при нажатии, например, Ctrl-C изменять язык на ``противоположный'' -- с русского на английский и наоборот.

На самом первом этапе текст программы выглядит так (``реализация'' интернационализации сделана на псевдокоде, который заключен в C-комментарии просто для того, чтобы программа компилировалась):

/* dummy1.c */

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

char *message = "This is a message";
char *description = "Ctrl-C handled, language switched. Press Ctrl-\\ to quit.";

void switch_message (int sig) {
  /*
   * если (текущий язык английский)
   *   установить (русский)
   * иначе
   *   установить (английский)
   */
  printf ("%s\n", /* перевести (*/ description /*)*/);
  message = /* перевести (*/ message /*)*/;
}

int main (int argc, char *argv[]) {
  signal (SIGINT, switch_message);

  while (1) {
    sleep (1);
    printf ("%s\n", message);
  }

  return 0;
}

Наша программа содержит две строковые константы, переводы которых нам нужно предоставить. Для генерации шаблона .po-файла воспользуемся программой xgettext пакета gettext. Эта программа извлекает из указанного множества .c- и .h- файлов строковые константы (специальным образом помеченные или может быть все) и создает нужный шаблон .po-файла:

[user@host:somedir] xgettext -a dummy.c -o dummy.po

Мы получили .po-файл, который выглядит следующим образом:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Free Software Foundation, Inc.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 1999-05-23 11:31+0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"

#: dummy1.c:8
msgid "This is a message"
msgstr ""

#: dummy1.c:9
msgid "Ctrl-C handled, language switched. Press Ctrl-\\ to quit."
msgstr ""

#: dummy1.c:18 dummy1.c:27
#, c-format
msgid "%s\n"
msgstr ""

Вначале идет некоторый заголовок, (формат его мы обсуждать не будем, однако поля рекомендуется заполнить),а далее последовательность элементов, которые устроены следующим образом:

  • после знака #: идет имя файла и номер строки исходного кода, где встречается соответствующая строка; таких вхождений может быть несколько; эта информация является комментарием и не включается в .mo-файл, однако избавляться от нее не следует;
  • ``msgid'' -- идентификатор сообщения, обьект, являющийся ключом при поиске перевода; роль ключа по соглашению играет английский вариант строки, хотя это в сущности все равно -- строковые константы в коде могут быть и русскими, и даже на разных языках;
  • ``msgstr'' -- ``перевод'' соответствующей строки.

С учетом всего сказанного создадим ``русский'' .po-файл, который назовем ru.po. Он будет выглядеть примерно так (заметим, что последнее вхождение не нуждается в переводе и может быть безболезненно удалено):

# Русский .po-файл для одной глупой программы.
# Copyright (C) 1999 Dummy Software Foundation, Inc.
# User (user@host), 1999.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: dummy 1.0\n"
"POT-Creation-Date: 1999-05-11 06:31+0400\n"
"PO-Revision-Date: 1999-4-14 11:51+2\n"
"Last-Translator: user <user@host>\n"
"Language-Team: ru <ru@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=8-bit\n"
"Content-Transfer-Encoding: koi8\n"

#: dummy1.c:12
msgid "This is a message"
msgstr "Это сообщение"

#: dummy2.c:13
msgid "Ctrl-C handled, language switched. Press Ctrl-\\ to quit."
msgstr "Перехвачен Ctrl-C, язык переключен. Для выхода нажмите Ctrl-C."

Соответственно ``английский'' .po-файл (en.po) выглядит так:

# Английский .po-файл для одной глупой программы.
# Copyright (C) 1999 Dummy Software Foundation, Inc.
# User (user@host), 1999.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: dummy 1.0\n"
"POT-Creation-Date: 1999-05-11 06:31+0400\n"
"PO-Revision-Date: 1999-4-14 11:51+2\n"
"Last-Translator: user <user@host>\n"
"Language-Team: ru <en@en.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=8-bit\n"
"Content-Transfer-Encoding: iso8859-1\n"

#: dummy.c:12
msgid "This is a message"
msgstr "This is a message"

#: dummy.c:13
msgid "Ctrl-C handled, language switched. Press Ctrl-\\ to quit."
msgstr "Ctrl-C handled, language switched. Press Ctrl-\\ to quit."

Теперь можно создавать .mo-файлы:

[user@host:somedir] msgfmt ru.po -o ru.mo
[user@host:somedir] msgfmt en.po -o en.mo

Данные локализации созданы. Теперь осталось только немного изменить программу.

``Перевод'' (по сути, поиск данных в соответствующем .mo-файле) выполняется функцией gettext(), прототип которой находится в заголовочном файле libintl.h. Если ``перевод'' будет найден, то он возвращается, иначе возвращается ключ. Это допускает, например, отсутствие английского .mo-файла (при условии, что все строковые константы в коде английские). Также будут использованы функции для установки locale, так что придется подключить еще и locale.h.

Функция gettext() для определения того, какой язык является установленным в данный момент, использует переменные окружения LANGUAGE, LC_ALL, LC_xyz (в соответствии с установленным locale), LANG (в порядке убывания приоритета). Будем использовать переменную LANGUAGE. Соответственно псевдокод в теле функции switch_language() нашей программы изменим так (обратите внимание, что первоначальное значение переменной message необходимо сохранить):

void switch_message (int sig) {
  char *lang;
  static char *message_key = "This is a message";
  if (strcmp ((lang = getenv ("LANGUAGE")), "ru") == 0)
    setenv ("LANGUAGE", "en", 1);
  else if (strcmp (lang, "en") == 0)
    setenv ("LANGUAGE", "ru", 1);
  printf ("%s\n", gettext (description));
  message = gettext (message_key);
}

Осталось только инициировать опереации локализации, т.е. указать, где находятся два наших файла с данными локализации и как они называются. Для этого используются функции textdomain() и bindtextdomain(). По соглашению, исходный код (обычно функция main()) содержитт их вызовы в таком порядке:

int main (int argc, char *argv[]) {
  /* ... */
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);
  /* ... */
}

Здесь PACKAGE -- имя программы, а LOCALEDIR -- имя директории, в соответствующих поддиректориях которой ожидается наличие .mo-файлов. Обычно эти макросы инициализируются в процессе конфигурации программы при запуске configure, потому что данные локализации по соглашению устанавливаются в $(prefix)/share/locale. Однако наша программа вряд ли будет устанавливаться куда-либо таким способом, поэтому в качестве имени программы (PACKAGE) пусть будет ``dummy'', a значением LOCALEDIR -- текушая рабочая директория, на которую далее будем ссылаться по имени wd. Данные локализации разумно вынести в отдельную директорию, которую назовем wd/localedir. Далее в этой директории нужно создать по отдельной директории для каждого .po-файла. Так как мы используем два значения LANGUAGE -- ``ru'' и ``en'',эти директории будут соответственно называться wd/localedir/en и wd/localedir/ru. Далее в каждой из этих директорий создается по директории со специальным именем LC_MESSAGES, в которые уже непосредственно помещается файл с данными локализации, имя которого есть значение макроса PACKAGE с расширением .mo, т.е dummy.mo в нашем случае. После всех этих манипуляций русскaя локализация должна находиться в wd/localedir/ru/LC_MESSAGES/dummy.mo, а английская соответственно в
wd/localedir/en/LC_MESSAGES/dummy.mo. Окончательный исходный текст программы выглядит так:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

#include <libintl.h>
#include <locale.h>

#define PACKAGE "dummy"
#define LOCALEDIR "localedir"

char *message = "This is a message";
char *description = "Ctrl-C handled, language switched. Press Ctrl-\\ to quit.";

void switch_message (int sig) {
  char *lang;
  static char *message_key = "This is a message";
  if (strcmp ((lang = getenv ("LANGUAGE")), "ru") == 0)
    setenv ("LANGUAGE", "en", 1);
  else if (strcmp (lang, "en") == 0)
    setenv ("LANGUAGE", "ru", 1);
  printf ("%s\n", gettext (description));
  message = gettext (message_key);
}

int main (int argc, char *argv[]) {

  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);
  signal (SIGINT, switch_message);

  message = gettext (message);

  while (1) {
    sleep (1);
    printf ("%s\n", message);
  }

  return 0;
}

Теперь установим переменную окружения LANGUAGE

[user@host:somedir] export LANGUAGE=ru
и запустим программу. Диалог с ней будет выглядеть примерно так:

[user@host:somedir] ./dummy
Это сообщение
Это сообщение
Это сообщение
[Нажатие Ctrl-C]
Ctrl-C handled, language switched. Press Ctrl-\ to quit.
This is a message
This is a message
This is a message
This is a message
This is a message
[Нажатие Ctrl-C]
Перехвачен Ctrl-C, язык переключен. Для выхода нажмите Ctrl-C.
Это сообщение
Это сообщение
[Нажатие Ctrl-C]
Ctrl-C handled, language switched. Press Ctrl-\ to quit.
This is a message
This is a message
This is a message
[Нажатие Ctrl-\]
Quit
[user@host:somedir]

Одно важное замечание. Вообще говоря, в этой программе нарушен принцип полной независимости исходных текстов от данных локализации -- исходный код содержит предположение, что имеются данные локализации для двух языков (английского и русского). Если бы кто-то написал для нашей программы еще одно locale, например французкое, то без изменения исходного текста программа не заговорила бы по-французки. Аналогичная ситуация наблюдается в программах с графическим интерфейсом. Допустим, имеется меню, с помощью которого предлагается выбрать язык. Один пункт меню выбирает соответствующий язык. Сколько же пунктов должно быть в этом меню ?

Напрашивается следующее решение. В процессе инициализации программы надо просмотреть всю директорию LOCALEDIR в поисках .mo-файлов. Так как они должны находиться в директориях, соответствующих названию языка (en, ru, fr и т.п.), то таким образом сначала можно определить, какие locale доступны, подсчитать их общее количество, а затем передать эти данные функции, создающей меню. Похожее решение можно было бы применить и в нашей программе.



Dmitry A. Antipov
1999-05-26