Данный раздел описывает неотъемлемую часть разработки интернационального продукта или адаптации продукта к местному рынку — Интернационализацию и Локализацию
Поскольку Интернационализация и Локализация длинные слова, будем использовать I18n и l10n, как и принято их сокращать.
Область влияния
Обсудим, что вообще из себя представляют эти два понятия, а, главное, где они используются и что нам, разработчикам, надо изменять для i18n-ии и l10n-ии проекта.
-
Интернационализация (
i18n) — приведение исходного кода в готовое для локализации состояниеI18nэто не локальное преобразование продукта, а его адаптация для потенциального использования в любом месте сообще.I18nпроводится на начальных этапах разработки. -
Локализация (
l10n) — адаптация культурных и языковых предпочтений ПО А вотl10nэто переводы и другие преобразования продукта для локальной площадки.
Для понимания необходимости локализации рассмотрим несколько объектов ПО, к которым она обычно необходима:
- Сообщения и тексты При работе с текстами основной проблемой являются словоформы. Системы падежей и исключений их них — дело непростое, а вот множественные формы вполне поддаются описанию. В каждом языке свои правила преобразования форм в зависимости от количества, также этих множественных форм может быть разное количество в разных языках (н-р, две множественные формы в английском и три в русском)
- Интерфейсные элементы Кроме перевода нужно также учитывать формат информации и особенности интерфейса. Например, для языков, написание в которых ведётся справа налево, нужно также адаптировать вывод
- Изображения Кроме текстовых изображение, которые, несомненно, также требуют локализации, разработчикам может понадобиться изменять культурную атрибуцию (например, дорожные знаки страны-оригинала на знаки страны-локализатора)
- Шрифты
Да, шрифты тоже надо адаптировать. А то будет очень грустно из красивого спецшрифта оригинала сделать
Comic Sans… - Документация, шаблоны документов и прочие непрограммные данные
И это далеко не весь список вещей, требующих локализации.
В Linux множество параметров, чувствительных к локали, можно посмотреть через команду locale:
user@localhost:~> locale
LANG=en_US.UTF-8 # Язык системы
LC_CTYPE=en_US.UTF-8
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8" # Формат вывода времени
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8" # Язык для вывода сообщений (то, с чем мы будем работать)
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
user@localhost:~>
Переводы GLibc
| Чтобы у вас в системе вообще заработали различные локали, необходимо поставить пакет переводов и их поддержки в базовой библиотеке Си — GLibc. В Debian-based дистрибутивах этот пакет называется [locales-all](https://packages.debian.org/buster/locales-all | locales-all). |
В дистрибутивах семейства Альт, пакет glibs-locales стоит всегда.
Мы будем заниматься только переводами непосредственно текстов и множественными формами.
GNU gettext
Для наших задач воспользуемся набором инструментов GNU gettext, который автоматически будет контролировать выбор словоформ при переводах и помогать нам генерировать переводы похожих элементов.
Обсудим, как происходит локализация программ на примере:
Рассмотрим примитивную программу, которая просто печатает строки
ahello.c
#include <stdio.h>
int main(void)
{
int i;
/* Simple text */
puts("Hello World\n");
for(i=0; i<5; i++)
/* Loosy plural example */
printf("Hello, %d World\n", i);
return 0;
}
user@localhost:~/LinuxAppDev/08_I18n/Lection> cc ahello.c
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./a.out
Hello World
Hello, 0 World
Hello, 1 World
Hello, 2 World
Hello, 3 World
Hello, 4 World
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Добавим простой autotools для сборки нашей программы
configure.ac
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.72])
AC_INIT([L10n gettext ex], [0.0], [user])
AC_INIT_AUTOMAKE([foreign])
AC_CONFIG_SRCDIR([ahello.c])
AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
Makefile.am
bin_PROGRAMS=ahello
Создание перевода
Первый шаг — интернационализация. Нам необходимо подготовить нашу программу к возможности перевода. Для этого все элементы, которые мы хотим локализовать, оборачиваем функциями из библиотеки locale.h, работающей в GNU gettext. Для краткости обозначим функцию-обёртку gettext через её классическое обозначение _.
Ещё один важный элемент интернационализации — домены переводов. Домен — это отдельное множество переведённых строк, изолированное от других переводов в ту же локаль. Домен необходим для разделения разных классов переводов в одном проекте. Например, в игровом приложении могут встречаться переводы реплик игровых персонажей (например, средневековых пиратов), а могут — переводы сообщений для администраторов или ведущих игру.
И добавим первую локализацию:
ahello.c
#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>
#include <libintl.h>
#include <locale.h>
#define _(STRING) gettext(STRING)
#define LOCALE_PATH "."
int main(int argc, char *argv[])
{
int i;
char *dir;
dir = dirname(realpath(argv[0], NULL));
setlocale (LC_ALL, "");
bindtextdomain ("ahello", LOCALE_PATH); // имя домена выбираем сами
textdomain ("ahello");
/* Simple text */
puts(_("Hello World\n"));
for(i=0; i<5; i++)
/* Loosy plural example */
printf(_("Hello, %d World\n"), i);
return 0;
}
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c configure.ac LICENSE Makefile.am
user@localhost:~/LinuxAppDev/08_I18n/Lection> autoreconf -fivs
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./configure
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 World
Hello, 1 World
Hello, 2 World
Hello, 3 World
Hello, 4 World
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Как мы видим, пока никакого волшебства не происходит. Посмотрим, что делает процесс при обработке программы:
user@localhost:~/LinuxAppDev/08_I18n/Lection> strace -e openat ./ahello 2> strace_data
Hello World
Hello, 0 World
Hello, 1 World
Hello, 2 World
Hello, 3 World
Hello, 4 World
user@localhost:~/LinuxAppDev/08_I18n/Lection> cat strace_data | grep -E "(LC)|(\.mo)"
openat(AT_FDCWD, "/usr/lib/locale/en_US.UTF-8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 3
<...>
openat(AT_FDCWD, "/home/user/LinuxAppDev/08_I18n/Lection/./en_US.UTF-8/LC_MESSAGES/ahello.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/user/LinuxAppDev/08_I18n/Lection/./en_US.utf8/LC_MESSAGES/ahello.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/user/LinuxAppDev/08_I18n/Lection/./en_US/LC_MESSAGES/ahello.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/user/LinuxAppDev/08_I18n/Lection/./en.UTF-8/LC_MESSAGES/ahello.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/user/LinuxAppDev/08_I18n/Lection/./en.utf8/LC_MESSAGES/ahello.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/user/LinuxAppDev/08_I18n/Lection/./en/LC_MESSAGES/ahello.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Наконец-то процесс пытается обращаться к каким-то .mo-файлам. Это те самые файлы переводов, которые создаются с помощью gettext по нашим переводам. Самое время написать их!
Следующий шаг после интернационализации программы — генерация общего шаблона для поиска переводимых фраз. Она делается с помощью команды xgettext, которой мы указываем ключ — название нашей функции перевода —, исходный объект и выходной шаблон. Принято файл выходного шаблона называть <src_name>.pot, т. к. он будет шаблоном для нашего файла переводов .po (соответственно, .po template).
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c configure.ac LICENSE Makefile.am
user@localhost:~/LinuxAppDev/08_I18n/Lection> xgettext -k_ -c ahello.c -o ahello.pot
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c ahello.pot configure.ac LICENSE Makefile.am
user@localhost:~/LinuxAppDev/08_I18n/Lection>
ahello.pot
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <user>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: 0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 17:03+0300\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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#. Simple text
#: ahello.c:21
msgid "Hello World\n"
msgstr ""
#. Loosy plural example
#: ahello.c:25
#, c-format
msgid "Hello, %d World\n"
msgstr ""
Получили заготовку для наших переводов. В ней пока не определён язык перевода, правила множественного перевода, нет информации о переводчиках и т.д.
Следующий шаг локализации — создание перевода. Для этого нам нужна команда msginit с указанием исходного шаблона, выходного файла переводов и локали перевода
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c ahello.pot configure.ac LICENSE Makefile.am
user@localhost:~/LinuxAppDev/08_I18n/Lection> msginit -i ahello.pot -o ru.po -l ru_RU.UTF-8
The new message catalog should contain your email address, so that users can
give you feedback about the translations, and so that maintainers can contact
you in case of unexpected technical problems.
Is the following your email address?
user@localhost
Please confirm by pressing Return, or enter your email address.
Retrieving https://translationproject.org/team/index.html... done.
Please visit your translation team's homepage at
https://translationproject.org/team/ru.html
https://translationproject.org/team/index.html
https://translationproject.org/html/translators.html
https://translationproject.org/html/welcome.html
and consider joining your translation team's mailing list
<gnu@d07.ru>
Created ru.po.
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c ahello.pot configure.ac LICENSE Makefile.am ru.po
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Шаблон перевода ….pot нужен для первоначального создания файлов с переводами на разные языки и для последующего их обновления. В процессе собственно перевода и компиляции он не участвует.
ru.po
# Russian translations for 0.0 package
# Английские переводы для пакета 0.0.
# Copyright (C) 2024 THE 0.0'S COPYRIGHT HOLDER
# This file is distributed under the same license as the 0.0 package.
# FIRST AUTHOR <user>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 17:03+0300\n"
"PO-Revision-Date: 2024-11-05 17:07+0300\n"
"Last-Translator: user <user@localhost>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"Language: ru\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"
#. Simple text
#: ahello.c:21
msgid "Hello World\n"
msgstr ""
#. Loosy plural example
#: ahello.c:25
#, c-format
msgid "Hello, %d World\n"
msgstr ""
Наш файл приобрёл локальное наполнение — добавились правила множественного вывода, явно указан язык перевода.
Однако переводов всё ещё нет (что логично, поля-то мы не заполнили никак)
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c configure.ac LICENSE Makefile.am ru.po
user@localhost:~/LinuxAppDev/08_I18n/Lection> autoreconf -fivs
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./configure
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 World
Hello, 1 World
Hello, 2 World
Hello, 3 World
Hello, 4 World
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Напишем переводы в наш файл
ru.po
# Russian translations for 0.0 package
# Английские переводы для пакета 0.0.
# Copyright (C) 2024 THE 0.0'S COPYRIGHT HOLDER
# This file is distributed under the same license as the 0.0 package.
# FIRST AUTHOR <user>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 17:03+0300\n"
"PO-Revision-Date: 2024-11-05 17:07+0300\n"
"Last-Translator: user <user@localhost>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"Language: ru\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"
#. Simple text
#: ahello.c:21
msgid "Hello World\n"
msgstr "Привет, мир\n"
#. Loosy plural example
#: ahello.c:25
#, c-format
msgid "Hello, %d World\n"
msgstr "Привет, %d миров\n"
Последний шаг для локализации — компиляция перевода. Именно на этом шаге мы создаём те самые .mo-файлы, к которым обращается процесс. При этом (вспоминаем вывод strace) нам нужно расположить эти файлы в правильном месте — директории <LOCALE_PATH>/<язык перевода>/LC_MESSAGES. Поскольку LOCALE_PATH у нас в программе определён как ".", переводы надо класть в текущую директорию.
За компиляцию перевода отвечает msgfmt.
Добавим в наш Makefile.am отдельный рецепт по компиляции перевода
Makefile.am
bin_PROGRAMS=ahello
ru/LC_MESSAGES/ahello.mo: ru.po
mkdir -p `dirname $@`
msgfmt $< -o $@
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c configure.ac LICENSE Makefile.am ru.po
user@localhost:~/LinuxAppDev/08_I18n/Lection> autoreconf -fivs
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./configure
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make ru/LC_MESSAGES/ahello.mo
mkdir -p `dirname ru/LC_MESSAGES/ahello.mo`
msgfmt ru.po -o ru/LC_MESSAGES/ahello.mo
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 World
Hello, 1 World
Hello, 2 World
Hello, 3 World
Hello, 4 World
user@localhost:~/LinuxAppDev/08_I18n/Lection> LC_ALL=ru_RU.UTF8 ./ahello
Привет, мир
Привет, 0 миров
Привет, 1 миров
Привет, 2 миров
Привет, 3 миров
Привет, 4 миров
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Ура, перевод состоялся!
Обновление Перевода
Давайте поменяем исходник и попробуем обновить уже существующий перевод:
ahello.c
#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>
#include <libintl.h>
#include <locale.h>
#define _(STRING) gettext(STRING)
#define LOCALE_PATH "."
int main(int argc, char *argv[])
{
int i;
char *dir;
dir = dirname(realpath(argv[0], NULL));
setlocale (LC_ALL, "");
bindtextdomain ("ahello", LOCALE_PATH);
textdomain ("ahello");
/* Simple text */
puts(_("Hello World\n"));
for(i=0; i<5; i++)
/* Loosy plural example */
printf(_("Hello, %d Worlds\n"), i); // Поменяли
return 0;
}
Для этого нам нужно вновь собрать переводимые фразы в шаблон с помощью xgettext, а после этого соединить их с уже существующим файлом перевода с помощью msgmerge, при этом:
- У нас сохранятся не изменившиеся переводы, они просто перенесутся в новый файл переводов;
- Автоматически для фраз, похожих на уже существовавшие в переводах, сгенерируется «нечёткий» перевод (помеченный значащим комментарием
# fuzzy)
user@localhost:~/LinuxAppDev/08_I18n/Lection> ls
ahello.c configure.ac LICENSE Makefile.am ru.po
user@localhost:~/LinuxAppDev/08_I18n/Lection> xgettext -k_ -c ahello.c -o ahello.pot
user@localhost:~/LinuxAppDev/08_I18n/Lection> msgmerge -U ru.po ahello.pot
.... done.
user@localhost:~/LinuxAppDev/08_I18n/Lection>
ru.po
# Russian translations for 0.0 package
# Английские переводы для пакета 0.0.
# Copyright (C) 2024 THE 0.0'S COPYRIGHT HOLDER
# This file is distributed under the same license as the 0.0 package.
# FIRST AUTHOR <user>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 17:27+0300\n"
"PO-Revision-Date: 2024-11-05 17:07+0300\n"
"Last-Translator: user <user@localhost>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"Language: ru\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"
#. Simple text
#: ahello.c:21
msgid "Hello World\n"
msgstr "Привет, мир\n"
#. Loosy plural example
#: ahello.c:25
#, fuzzy, c-format
msgid "Hello, %d Worlds\n"
msgstr "Привет, %d миров\n"
Кстати, это даже более культурный перевод! Но он fuzzy, т.е. считается возможно некорректным, поэтому заблокирован.
user@localhost:~/LinuxAppDev/08_I18n/Lection> autoreconf -fivs
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./configure
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make ru/LC_MESSAGES/ahello.mo
mkdir -p `dirname ru/LC_MESSAGES/ahello.mo`
msgfmt ru.po -o ru/LC_MESSAGES/ahello.mo
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 Worlds
Hello, 1 Worlds
Hello, 2 Worlds
Hello, 3 Worlds
Hello, 4 Worlds
user@localhost:~/LinuxAppDev/08_I18n/Lection> LC_ALL=ru_RU.UTF8 ./ahello
Привет, мир
Hello, 0 Worlds
Hello, 1 Worlds
Hello, 2 Worlds
Hello, 3 Worlds
Hello, 4 Worlds
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Убираем fuzzy и всё заработает
ru.po
# Russian translations for 0.0 package
# Английские переводы для пакета 0.0.
# Copyright (C) 2024 THE 0.0'S COPYRIGHT HOLDER
# This file is distributed under the same license as the 0.0 package.
# FIRST AUTHOR <user>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 17:27+0300\n"
"PO-Revision-Date: 2024-11-05 17:07+0300\n"
"Last-Translator: user <user@localhost>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"Language: ru\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"
#. Simple text
#: ahello.c:21
msgid "Hello World\n"
msgstr "Привет, мир\n"
#. Loosy plural example
#: ahello.c:25
#, c-format
msgid "Hello, %d Worlds\n"
msgstr "Привет, %d миров\n"
user@localhost:~/LinuxAppDev/08_I18n/Lection> make ru/LC_MESSAGES/ahello.mo
mkdir -p `dirname ru/LC_MESSAGES/ahello.mo`
msgfmt ru.po -o ru/LC_MESSAGES/ahello.mo
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 Worlds
Hello, 1 Worlds
Hello, 2 Worlds
Hello, 3 Worlds
Hello, 4 Worlds
user@localhost:~/LinuxAppDev/08_I18n/Lection> LC_ALL=ru_RU.UTF8 ./ahello
Привет, мир
Привет, 0 миров
Привет, 1 миров
Привет, 2 миров
Привет, 3 миров
Привет, 4 миров
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Отлично, с основными задачами — создание и обновление алгоритма — разобрались. Теперь для удобства опишем зависимость нашей программы от файла переводов для его автоматической генерации в Makefile
Makefile.am
bin_PROGRAMS=ahello
pkgdata_DATA=ru/LC_MESSAGES/ahello.mo
ru/LC_MESSAGES/ahello.mo: ru.po
mkdir -p `dirname $@`
msgfmt $< -o $@
ru.po: ahello.pot
msgmerge -U $@ $<
ahello.pot: ahello.c
xgettext -k_ -c ahello.c -o ahello.pot
Множественные переводы
До этого мы занимались просто переводами без учёта множественности.
В английском языке множественная форма одна, а в русском — три! Для них в нашем po-файле специально написан тернарный оператор для расчёта формы.
Перенесём множественные переводы в нашу программу. Для этого проведём интернационализацию, учитывающую множественные переводы, добавив для множественных элементов функцию ngettext
ahello.c
#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>
#include <libintl.h>
#include <locale.h>
#define _(STRING) gettext(STRING)
#define LOCALE_PATH "."
int main(int argc, char *argv[])
{
int i;
char *dir;
dir = dirname(realpath(argv[0], NULL));
setlocale (LC_ALL, "");
bindtextdomain ("ahello", LOCALE_PATH);
textdomain ("ahello");
/* Simple text */
puts(_("Hello World\n"));
for(i=0; i<6; i++)
/* Plural example */
printf(ngettext("Hello, %d World\n", "Hello, %d Worlds\n", i), i);
return 0;
}
user@localhost:~/LinuxAppDev/08_I18n/Lection> xgettext -k_ -c ahello.c -o ahello.pot
user@localhost:~/LinuxAppDev/08_I18n/Lection> msgmerge -U ru.po ahello.pot
.... done.
user@localhost:~/LinuxAppDev/08_I18n/Lection>
ru.po
# Russian translations for 0.0 package
# Английские переводы для пакета 0.0.
# Copyright (C) 2024 THE 0.0'S COPYRIGHT HOLDER
# This file is distributed under the same license as the 0.0 package.
# FIRST AUTHOR <user>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 17:41+0300\n"
"PO-Revision-Date: 2024-11-05 17:07+0300\n"
"Last-Translator: user <user@localhost>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"Language: ru\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"
#. Simple text
#: ahello.c:21
msgid "Hello World\n"
msgstr "Привет, мир\n"
#. Plural example
#: ahello.c:25
#, c-format
msgid "Hello, %d World\n"
msgid_plural "Hello, %d Worlds\n"
msgstr[0] "Привет, %d мир\n"
msgstr[1] "Привет, %d мирa\n"
msgstr[2] "Привет, %d миров\n"
user@localhost:~/LinuxAppDev/08_I18n/Lection> autoreconf -fivs
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./configure
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 Worlds
Hello, 1 World
Hello, 2 Worlds
Hello, 3 Worlds
Hello, 4 Worlds
Hello, 5 Worlds
user@localhost:~/LinuxAppDev/08_I18n/Lection> LC_ALL=ru_RU.UTF8 ./ahello
Привет, мир
Привет, 0 миров
Привет, 1 мир
Привет, 2 мирa
Привет, 3 мирa
Привет, 4 мирa
Привет, 5 миров
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Автоподдержка gettext
У GNU есть собственная поддержка gettext-а. Так что мы можем создать отдельный каталог для переводов и исходников, которые отлично соберутся вместе при локализации.
Заодно распилим Makefile.am на части
user@localhost:~/LinuxAppDev/08_I18n/Lection> tree
.
├── configure.ac
├── LICENSE
├── Makefile.am
├── po
│ ├── Makefile.am
│ └── ru.po
└── src
├── ahello.c
└── Makefile.am
3 directories, 7 files
user@localhost:~/LinuxAppDev/08_I18n/Lection> cat Makefile.am src/Makefile.am po/Makefile.am
SUBDIRS = src po
###################
bin_PROGRAMS=ahello
###################
RU = ru/LC_MESSAGES
PACKAGE = ahello
pkgdata_DATA = $(RU)/$(PACKAGE).mo
$(RU)/$(PACKAGE).mo: ru.po
mkdir -p `dirname $@`
msgfmt $< -o $@
$(PACKAGE).pot: ../src/$(PACKAGE).c
xgettext -k_ -c $< -o $@
ru.po: $(PACKAGE).pot
msgmerge -U $@ $<
user@localhost:~/LinuxAppDev/08_I18n/Lection> cat configure.ac
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.72])
AC_INIT([L10n gettext ex], [0.0], [user])
AM_INIT_AUTOMAKE([foreign])
AC_CONFIG_SRCDIR([src/ahello.c])
AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_CONFIG_FILES([Makefile po/Makefile src/Makefile])
AC_OUTPUT
user@localhost:~/LinuxAppDev/08_I18n/Lection>
user@localhost:~/LinuxAppDev/08_I18n/Lection> autoreconf -fivs
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./configure
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> make
<...>
user@localhost:~/LinuxAppDev/08_I18n/Lection> ./ahello
Hello World
Hello, 0 Worlds
Hello, 1 World
Hello, 2 Worlds
Hello, 3 Worlds
Hello, 4 Worlds
Hello, 5 Worlds
user@localhost:~/LinuxAppDev/08_I18n/Lection> LC_ALL=ru_RU.UTF8 ./ahello
Привет, мир
Привет, 0 миров
Привет, 1 мир
Привет, 2 мирa
Привет, 3 мирa
Привет, 4 мирa
Привет, 5 миров
user@localhost:~/LinuxAppDev/08_I18n/Lection>
Воспользуемся intltool, чтобы ручками не пилить всё
Рассмотрим пример с прошлой лекции и на него наложим переводы
user@localhost:~/LinuxAppDev/08_I18n/Lection/key_po> tree
.
├── configure.ac
├── Makefile.am
├── po
│ ├── POTFILES.in
│ └── ru.po
└── src
└── key.c
3 directories, 5 files
user@localhost:~/LinuxAppDev/08_I18n/Lection/key_po> cat po/POTFILES.in
src/key.c
user@localhost:~/LinuxAppDev/08_I18n/Lection/key_po> cat po/ru.po
# Russian translations for Key logger package
# Английские переводы для пакета Key logger.
# Copyright (C) 2024 THE Key logger'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Key logger package.
# user <user@localhost>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: Key logger 0.00\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-05 15:02+0000\n"
"PO-Revision-Date: 2024-11-04 20:31+0000\n"
"Last-Translator: user <user@localhost>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"Language: ru\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/key.c:22
msgid "Not a terminal"
msgstr "Это не терминал"
#: ../src/key.c:37
#, c-format
msgid " Key: %d, Name: %s\n"
msgstr " Клавиша: %d, Имя: %s\n"
src/key.c
#include <ncurses.h>
#include <unistd.h>
#include <error.h>
#include <libintl.h>
#include <locale.h>
#include "config.h"
#ifndef DX
#define DX 3
#endif
#define _(STRING) gettext(STRING) # добавили интернационализацию
int main(void) {
WINDOW *win;
int c = 0;
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
if (!isatty(0))
error(1, 0, _("Not a terminal"));
initscr();
noecho();
cbreak();
printw("%s (%s)", PACKAGE_STRING, PACKAGE_BUGREPORT);
refresh();
win = newwin(LINES-2*DX, COLS-2*DX, DX, DX);
#ifdef KEYPAD
keypad(win, TRUE);
#else
keypad(win, FALSE);
#endif
scrollok (win, TRUE);
do {
wprintw(win, _(" Key: %d, Name: %s\n"), c, keyname(c));
box(win, 0, 0);
wrefresh(win);
} while((c = wgetch(win)) != 27);
endwin();
return 0;
}
Makefile.am
CFLAGS = -Wall -O0 -g
AM_CPPFLAGS = -D'LOCALEDIR="$(localedir)"'
bin_PROGRAMS=key
key_SOURCES=src/key.c
SUBDIRS = po
TESTS = isterm.sh
isterm.sh:
echo 'test "`./key < /dev/null 2>&1`" = "./key: Not a terminal"' > $@
chmod +x $@
gitclean:
git clean -xdf
configure.ac
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.69])
AC_INIT([Key logger], [0.00], [george@altlinux.org])
AM_INIT_AUTOMAKE([foreign subdir-objects])
AM_GNU_GETTEXT(external)
AM_GNU_GETTEXT_VERSION(0.21)
AC_CONFIG_SRCDIR([src/key.c])
AC_CONFIG_HEADERS([config.h])
AC_SUBST(GETTEXT_PACKAGE, "$PACKAGE")
ALL_LINGUAS="ru"
# Checks for programs.
AC_PROG_CC
IT_PROG_INTLTOOL
# Checks for libraries.
AC_CHECK_LIB([ncursesw],[initscr],[],[AC_MSG_ERROR([Curses is needed])])
AC_CHECK_LIB([optional],[completely_lost])
# Checks for header files.
AC_CHECK_HEADERS([unistd.h])
AC_CHECK_HEADER([ncurses.h])
# Optional clues
AC_ARG_ENABLE([keypad],
AS_HELP_STRING([--enable-keypad], [Enable input ESC-sequences recognition]),
AC_DEFINE(KEYPAD,[],[ESC-sequences recognition])
)
AC_ARG_ENABLE([frame],
AS_HELP_STRING([--enable-frame=width], [Define frame width]),
AC_DEFINE_UNQUOTED(DX,[$enable_frame],[Frame width])
)
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_FUNC_ERROR_AT_LINE
AC_CONFIG_FILES([Makefile po/Makefile.in])
AC_OUTPUT
Домашнее задание
- Написать довольно тупую программу на Си, которая угадывает число
1…100методом половинного деления- Первым делом просит загадать число от 1 до 100
- Затем спрашивает «число больше 50?» (ответ тоже надо переводить)
- На основании ответа выбирает пол-интервала и повторяет его по циклу
- Когда догадается — отвечает
- Оформить автосборку вокруг неё (
autotools/CMake/Meson/как хотите) - Добавить перевод
- Добавить
- Либо поддержку перевода с помощью
autotools - Либо автоматическое обновление/пересборку перевода и удаление генератов
- Обратите внимание:
.po-файл (а в примере до перехода наautotoolsещё заголовочная часть.pot-файла) — не генераты;.mo, временный.potи прочее — генераты
- Либо поддержку перевода с помощью