View on GitHub

Methodics of GNU/Linux application software development

Practical materials for learning application software development on Linux

Данный раздел описывает неотъемлемую часть разработки интернационального продукта или адаптации продукта к местному рынку — Интернационализацию и Локализацию

Поскольку Интернационализация и Локализация длинные слова, будем использовать I18n и l10n, как и принято их сокращать.

Область влияния

Обсудим, что вообще из себя представляют эти два понятия, а, главное, где они используются и что нам, разработчикам, надо изменять для i18n-ии и l10n-ии проекта.

Для понимания необходимости локализации рассмотрим несколько объектов ПО, к которым она обычно необходима:

И это далеко не весь список вещей, требующих локализации.

В 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, при этом:

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. Написать довольно тупую программу на Си, которая угадывает число 1…100 методом половинного деления
    • Первым делом просит загадать число от 1 до 100
    • Затем спрашивает «число больше 50?» (ответ тоже надо переводить)
    • На основании ответа выбирает пол-интервала и повторяет его по циклу
    • Когда догадается — отвечает
  2. Оформить автосборку вокруг неё (autotools/CMake/Meson/как хотите)
  3. Добавить перевод
  4. Добавить
    • Либо поддержку перевода с помощью autotools
    • Либо автоматическое обновление/пересборку перевода и удаление генератов
    • Обратите внимание: .po-файл (а в примере до перехода на autotools ещё заголовочная часть .pot-файла) — не генераты; .mo, временный .pot и прочее — генераты