Интернационализация BASH-скриптов

Материал из Kalina_LUG_Wiki
Перейти к: навигация, поиск

Вступ

Обычно скрипты пишутся на одном (человеческом) языке. В советских реалиях это либо русский, либо английский. Но иногда возникает необходимость задуматься об интернационализации написанной программы. XXI век на дворе, всё-таки.

Вольный перевод этой статьи в адаптации к Ubuntu Lucid.

Основные понятия

  • Каталог сообщений (Message Catalog) --- индексированный репозиторий сообщений на человеческом языке, используемых интернационализованными приложениями. Каталог сообщений используется для разделения фраз на человеческом языке и программного кода. Несмотря на название "каталог", представляет собой файл.
  • Интернационализация (Internationalization, i18n, i--18 букв--n) --- по большому счёту, это набор (последовательность) действий, которые разработчики программ предпринимают для того, чтобы их программа не зависела от используемого языка системы оконечного пользователя. На этапе разработки сообщения на человеческом языке и собственно код никогда не смешиваются! Код приложения обращается к каталогу сообщений за текстом на человеческом языке. Для этого используются уникальные ключи каталога сообщений.
  • Локализация (Localization, l10n, l--10 букв--n) --- процесс адаптирования интерфейса программы к конкретному человеческому языку. Когда применена I18N, нет необходимости переписывать код программы. Нужно сконцентрироваться на переводе сообщений из каталога на целевые человеческие языки.
  • Язык системы или Локаль (Locale) --- часть системных переменных на пользовательской системе, определяющих язык системы, страну и региональные настройки. Локаль устанавливается и настраивается как часть операционной системы пользователя.

Подготовка BASH-скрипта к i18n

Какие же элементы скрипта чувствительны к используемому человеческому языку? Те, которые видит пользователь за работой с программой. То есть:

  • текстовые вопросы к пользователю;
  • сообщения об ошибках;
  • индикаторы прогресса и/или отладочные сообщения, выводимые в системные журналы или на консоли пользователя;
  • справка, интерактивная документация и другая полезная информация.

Рассмотрим пример. Это скрипт, который нельзя назвать интернационализованным. Он не имеет практической ценности, но как пример для нашей темы сойдёт чуть более, чем полностью. Скрипт выводит случайное число в указанных пользователем пределах и выводит информацию о своей активности.

Файл rand.sh:

#!/bin/bash

function random {
        typeset low=$1 high=$2
        echo $(( ($RANDOM % ($high - $low) ) + $low ))
}

# (1)
echo  "Hello, I can generate a random number between 2 numbers that you provide"
#(2)
echo -n "What is your low number? "
read low
#(3) 
echo -n "What is your high number? "
read high

if [[ $low -ge $high ]]
then
        #(4)
        echo "1st number should be lower than the second - leaving early." >&2
        exit 1
fi

rand=$(random $low $high )
#(5)
echo "from/to generated (by/at):  $low / $high $rand (${LOGNAME} / $(date))" >> /tmp/POC
#(6)
echo "Your Random Number Is: $rand "

exit 0

Выполнение этого скрипта выводит следующие сообщения на консоли пользователя:

$: rand.sh
Hello, I can generate a random number between 2 numbers that you provide
What is your low number? 50
What is your high number? 125
Your Random Number Is: 95
$: 

Строки, прокомментированные цифрами от (1) до (6), нуждаются в замене. Они содержат сообщения на человеческом языке. Эти сообщения необходимо перенести в каталог сообщений.

Чтобы пояснить формат каталога сообщений, рассмотрим пример. Файл содержит два сообщения: приветствие и сообщение об ошибке. Основа формата -- пара ключ--значение. Часть, начинающаяся с "msgid" -- ключ, с "msgstr" -- сообщение на человеческом языке, в данном случае на американском английском.

Пример файла en.po

msgid "Main Greeting" 
msgstr "Welcome, what do you want to do today?" 
msgid "Missing File Error" 
msgstr "File Not Found"

Каталоги сообщений можно составить "руками", специальным образом подготовить и установить в оконечной системе для использования более, чем одной программой. Такие каталоги сообщений называются "переносимые объекты" (portable objects) -- обратите внимание на расширение файла (.po).

Давайте составим каталог сообщений для нашего скрипта. Смотрим на комментарии с цифрами в коде скрипта и пишем:

Файл en.po

msgid "Greeting"
msgstr "Hello, I can generate a random number between 2 numbers that you provide"
msgid "Low Number Prompt"
msgstr "What is your low number"
msgid "High Number Prompt"
msgstr "What is your high number"
msgid "Input Error"
msgstr "1st number should be lower than the second - leaving early."
msgid "Result Title"
msgstr "Your Random Number Is: "
msgid "Activity Log"
msgstr "from/to generated (by/at): "

Теперь, когда мы разобрались с тем, как составлять каталог сообщений, составим его для русского языка:

Файл ru.po

msgid "Greeting"
msgstr "Привет, я могу сгенерировать случайное число в указанном Вами диапазоне."
msgid "Low Number Prompt"
msgstr "Каков будет нижний предел?"
msgid "High Number Prompt"
msgstr "Каков будет верхний предел?"
msgid "Input Error"
msgstr "Нижний предел должен быть меньше верхнего! Выходим..."
msgid "Result Title"
msgstr "Случайное число:"
msgid "Activity Log"
msgstr "от/до число (пользователь/время): "

Обтратите внимание: значения строк, начинающихся с msgid, не изменились.

Когда каталоги сообщений подготовлены, необходимо сконвертировать их в формат 'message object files' (.mo). Для этого используем утилиту msgfmt (в Ubuntu в пакете gettext):

msgfmt -o rand.sh.mo ru.po
cp -p rand.sh.mo $HOME/locale/ru/LC_MESSAGES/ 
msgfmt -o rand.sh.mo en.po
cp -p rand.sh.mo $HOME/locale/en/LC_MESSAGES/

Каталоги сообщений для двух языков установлены в домашнюю директорию пользователя. Для того, чтобы было понятно, как BASH-скрипт будет использовать установленные каталоги сообщений, необходимо упомянуть об утилите gettext из одноимённого пакета.

Системные каталоги сообщений находятся в директории /usr/lib/locale. Вот как выглядит листинг этой директории на моей системе:

en_AG
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_NG
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZW.utf8
ru_RU.utf8
ru_UA.utf8

Мы же установили каталоги сообщений для нашего скрипта в домашнюю директорию пользователя. Для указания пути к каталогам сообщений существует системная переменная TEXTDOMAINDIR.

Получение сообщения из каталога сообщений (работает только при выполнении из скрипта, просто так из командной строчки не сработает --Ua2fga 14:56, 30 сентября 2010 (MSD)):

$: locale
LANG=ru_RU.utf8
LANGUAGE=ru_RU:ru
LC_CTYPE="ru_RU.utf8"
LC_NUMERIC="ru_RU.utf8"
LC_TIME="ru_RU.utf8"
[ ... кусь ... кусь ...]
$: export TEXTDOMAINDIR=/home/artem/locale
$: gettext -s "Greeting"
Привет, я могу сгенерировать случайное число в указанном Вами диапазоне.
$:

Изменим язык (локаль) системы на английскую:

$: export LC_ALL="en_US.utf8"
$: export LANGUAGE="en_US:en"
$: locale
LANG=en_US.utf8
LANGUAGE=en_US:en
LC_CTYPE="en_US.utf8"
LC_NUMERIC="en_US.utf8"
LC_TIME="en_US.utf8"
[ ... кусь ... кусь ...]
$: export TEXTDOMAINDIR=/home/artem/locale
$: gettext -s "Greeting"
Hello, I can generate a random number between 2 numbers that you provide
$:

Таким образом мы выяснили, что утилиты msgfmt и gettext -- основа i18n и l10n в BASH. Как же нам модифицировать исходный скрипт для поддержки разных языков?

Для удобства набросаем маленький скриптик. Он выставляет значение переменной TEXTDOMAINDIR и декларирует 4 полезных функции. Сделано это по двум причинам: во-первых, это изолирует лишние детали из кода исходного скрипта, а, во-вторых, он может быть использован повторно в случаях, когда необходимо выполнить действия с сообщениями на человеческом языке:

  • вывести текст через стандартный поток вывода (STDOUT);
  • отобразить сообщение об ошибке через стандартный поток ошибок (STDERR);
  • запросить ответ пользователя;
  • вывести сообщение в файл (системный журнал).

Файл i18n-lib.sh:

#!/bin/bash
##
# Thin library around basic I18N facilitated function
#   basic text display, file logging, error display, and prompting
export TEXTDOMAINDIR=/home/artem/locale

###############################################
##
## Display some text to stderr
## $1 is assumed to be the Message Catalog key
function i18n_error {
        echo "$(gettext -s "$1")" >&2
}

###############################################
##
## Display some text to sdtout
## $1 is assumed to be the Message Catalog key
## rest of args are used as misc information
function i18n_display {
        typeset key="$1"
        shift
        echo "$(gettext -s "$key") $@"
}

###############################################
## Append a log message to a file.
## use $1 as target file to append to
## use $2 as catalog key
## rest of args are used as misc information
function i18n_fileout {
        [[ $# -lt 2 ]] && return 1
        typeset file="$1"
        typeset key="$2"
        shift 2
        echo "$(gettext -s "$key") $@" >> ${file}
}

## Prompt the user with a message and echo back the response.
## $1 is assumed to be the Message Catalog key
function i18n_prompt {
        typeset rv
        [[ $# -lt 1 ]] && return 1
        read -p "$(gettext "$1"): " rv
        echo $rv
}

Теперь настала необходимость изменить исходный скрипт, добавить поддержку мультиязычности. Для этого внесём 4 изменения:

  1. В переменную окружения TEXTDOMAIN запишем путь к исходному скрипту;
  2. Включим вспомогательный скрипт i18n-lib.sh;
  3. Предоставим пользователю возможность выбирать английский язык;
  4. Все выражения "echo", содержащие текст на человеческом языке, заменяются вызовами функций из i18n-lib.sh.

Текст модифицированного скрипта:

Файл i18n-rand.sh:

#!/bin/bash
##
# POC around i18n/Localization in a bash script
#(1)
export TEXTDOMAIN=rand.sh
I18NLIB=i18n-lib.sh
#(2)
# source in I18N library - shown above
if [[ -f $I18NLIB ]]
then
        . $I18NLIB
else
        echo "ERROR - $I18NLIB NOT FOUND"
        exit 1
fi

## Start of example script 
function random {
        typeset low=$1 high=$2
        echo $(( ($RANDOM % ($high - $low) ) + $low ))
}
#(3)
## ALLOW USER TO SET LANG PREFERENCE
## assume lang and country code follows
if [[ "$1" = "-lang" ]]
then
        export LANGUAGE="$2_$3"
        export LC_ALL="$2_$3.utf8"
fi

#(4) 
# Display initial greeting
i18n_display "Greeting"
# ask for input 
low=$(i18n_prompt "Low Number Prompt" )
high=$(i18n_prompt "High Number Prompt" )
# check for error condition and display error if found 
if [[ $low -ge $high ]]
then
        i18n_error "Input Error"
        exit 1
fi
rand=$(random $low $high )
# Log what was just done 
i18n_fileout "/tmp/POC" "Activity Log" "$low / $high $rand (${LOGNAME} / $(date))"
# Display Results 
i18n_display "Result Title" $rand
exit 0


В доказательство того, что этот интерционализованный скрипт работает, проведём два испытания, по одному для русского и английского языков:

$: i18n-rand.sh -lang en US
Hello, I can generate a random number between 2 numbers that you provide
What is your low number?  100
What is your high number?  1000
Your Random Number Is:  615
$:  i18n-rand.sh
Привет, я могу сгенерировать случайное число в указанном Вами диапазоне.
Каков будет нижний предел?  500
Каков будет верхний предел?  1000
Случайное число: 601
$: 

Содержимое журнала --- на двух языках. Стандартная команда date уже интернационализована без нас.

Файл /tmp/POC:

from/to generated (by/at):  50 / 125 95 (artem / Wed Sep 29 12:57:38 EEST 2010)
from/to generated (by/at):  100 / 1000 615 (artem / Wed Sep 29 12:57:59 EEST 2010)
от/до число (пользователь/время): 500 / 1000 601 (artem / Срд Сен 29 12:58:48 EEST 2010)


Источник