Мы уже говорили, что тип символа, наверное, самый важный из всех. Но что такое символьные данные? Какие символы? Из какого алфавита? Какой язык? Какие культурные особенности?
В прошлом в вычислительной технике и информатике применялся исключительно английский язык. Традиция восходит, вероятно, еще к Чарльзу Беббиджу. Это не так уж и плохо, ведь надо же с чего-то начинать, а алфавит из 26 букв без диакритических знаков — неплохое начало.
Но теперь компьютеры распространились повсеместно. Наверное, в каждой стране есть хотя бы несколько компьютеров и тот или иной вид доступа в сеть. Естественно, любой человек предпочитает читать Web-страницы, электронную почту и прочие данные на своем родном языке, а не только на английском.
Человеческие языки поразительно разнообразны. Некоторые являются почти фонетическими, к другим это определение применимо с большой натяжкой. В одних есть настоящий алфавит, другие же предлагают набор из нескольких тысяч символов, ведущих происхождение от пиктограмм. В некоторых языках не один алфавит, а несколько. На каких-то языках пишут сверху вниз, на других — справа налево. Некоторые алфавиты очень просты, в других ряд букв украшен приводящими в трепет точечками, черточками, кружочками, галочками и штрихами… Есть языки, где при определенных условиях соседние буквы комбинируются; иногда это обязательно, а иногда и нет. В каких-то языках предусмотрено различение строчных и прописных букв, но большинство таких различий не устанавливает.
За 25 лет мы прошли длинный путь. Мы худо-бедно научились приводить в порядок хаотическое нагромождение символов и языков.
Если вам часто приходится иметь дело с программами, спроектированными для работы в различной языковой среде, то вы знаете, что такое интернационализация. Это способность программы поддерживать более одного естественного языка.
С интернационализацией тесно связаны мультиязычность и локализация. Почему-то принято сокращать эти слова, удаляя средние буквы и подставляя вместо них число, равное количеству удаленных букв:
def shorten(str)
(str[0..0] + str[1..-2].length.to_s + str[-1..-1]).upcase
end
shorten("internationalization") # I18N
shorten("multilingualization") # M17N
shorten("localization") # L10N
Термины I18N и M17N — практически синонимы; еще говорят «глобализация», но это слово имеет и другой смысл. Термин L10N более широкий: он подразумевает полную поддержку местной культуры и соглашений (например, символов обозначения валюты, способов форматирования даты и времени, использования точки или запятой для отделения целой и дробной частей в десятичном числе и многое другое).
Начнем с терминологии, поскольку в этой области широко используется жаргон. Заодно совершим небольшой экскурс в историю, так как текущее состояние дел становится понятным, лишь если рассмотреть его в контексте медленной эволюции. Уроки истории будут сведены к минимуму.
4.1. Исторические сведения и терминология
В «недобрые старые дни» становления компьютерных технологий, примерно совпадающие по времени с периодом использования перфокарт, существовало множество наборов символов. К счастью, с появлением кода ASCII в 1970-х годах эти дни миновали безвозвратно.
Аббревиатура ASCII означает American Standard Code for Information Interchange (Американский стандартный код обмена информацией). Это был большой шаг вперед, однако ключевое слово здесь «американский». Код проектировался даже без учета европейских языков, не говоря уже об азиатских.
Но в нем были и огрехи. Набор символов ASCII состоит из 128 символов (он 7-разрядный). Но как можно так расточительно относиться к дополнительному биту? Возникла естественная идея расширить набор ASCII, воспользовавшись кодами от 128 до 255 для других целей. Беда в том, что эта идея была реализована многократно и по-разному компанией IBM и другими. Не было общепринятого соглашения о том, какому символу соответствует, например, код 221.
Недостатки такого подхода очевидны. Даже если отправитель и получатель договорятся об используемом наборе символов, все равно они не смогут общаться на нескольких языках: для всех сразу не хватит символов. Если вы хотите писать по-немецки, но вставить в текст несколько цитат на греческом или иврите, то, скорее всего, ничего не получится. И эта схема не позволила даже приблизиться к решению проблем, связанных с азиатскими языками, например китайским, японским и корейским.
Было два основных способа решить эту задачу. Первый — использовать гораздо более обширный набор символов, например представляя каждый символ 16 битами (так называемые широкие символы). Второй — обратиться к многобайтовым кодировкам переменной длины. При такой схеме одни символы представляются единственным байтом, другие — двумя, а третьи — тремя или даже большим числом. При этом, очевидно, возникает масса вопросов. В частности, любая строка должна однозначно декодироваться. Первый байт многобайтового символа мог бы принадлежать специальному классу, а потому мы сумели бы понять, что следует ожидать дополнительный байт; но как быть со вторым и последующими? Разрешено ли им перекрываться с набором однобайтовых символов? Могут ли определенные символы выступать в роли второго и третьего байта или это следует запретить? Сможем ли мы перейти в середину строки и при этом не запутаться? Сможем ли просматривать строку в обратном направлении? Для разных кодировок были приняты различные проектные решения.
В конечном счете родилась идея кодировки Unicode. Считайте, что это «всемирный набор символов». Увы, на практике все не так просто.
Возможно, вы слышали, что Unicode был (или остается) ограничен 65536 символами (именно столько различных комбинаций можно представить 16 битами). Распространенное заблуждение!.. При проектировании Unicode такие ограничения не закладывались. С самого начала было ясно, что во многих случаях это будет многобайтовая схема. Количество представимых с помощью Unicode символов практически безгранично, и это хорошо, так как 65000 никогда не хватит для всех языков мира.
Говоря об интернационализации, нужно прежде всего понимать, что интерпретация строки не является внутренне присущей самой строке. Это заблуждение проистекает из уже неактуального представления, будто существует лишь один способ хранения строки.
Подчеркну, это исключительно важное положение. Внутренне строка — всего лишь последовательность байтов. Представьте себе, что в памяти машины хранится один байт в кодировке ASCII. Если это буква, которую мы называем «прописная латинская А», то реально хранится число 65.
Почему мы считаем, что 65 — это А? Потому что так мы договорились использовать (интерпретировать) это значение. Если мы складываем его с другим числом, то оно используется (интерпретируется) как число. А если отправляем его на терминал по последовательной линии связи — значит, интерпретируем как ASCII-символ.
Если можно по-разному интерпретировать одиночный байт, то почему же нельзя так сделать для последовательности байтов? На самом деле, чтобы получилась осмысленная строка, предполагаемая схема интерпретации (или кодировка должна быть известна заранее. Кодировка — это просто соответствие между двоичными числами и символами. И снова не все так просто.
Поскольку Ruby появился в Японии, он прекрасно справляется с двумя различными японскими кодировками (и ASCII). Не буду тратить время на рассказ о поддержке японского языка; если вы японец, то в вашем распоряжении сколько угодно книг по Ruby на этом языке. А для всех остальных наиболее распространённой кодировкой является Unicode. О ней мы и будем говорить в этой главе.
Но перед тем как перейти к деталям, познакомимся с некоторыми терминами. Называть вещи полезными именами — одна из основ мудрости!
• Байт — это просто восемь битов (хотя когда-то даже это было неверно). По традиции многие считают, что байт соответствует одному символу. Ясно, что в контексте I18N это не так.
• Кодовая позиция — один элемент воображаемой таблицы, с помощью которой представляется набор символов. Хотя это и не совсем верно, можете считать, что кодовые позиции взаимно однозначно отображаются на символы. Точнее будет сказать, что иногда для уникального указания символа требуется несколько кодовых позиций.
• Глиф (печатный знак) — визуальное представление кодовой позиции. Хотя интуитивно это и не совсем очевидно, символ и его визуальное представление - разные вещи. (Я могу открыть текстовый редактор и набрать прописную А десятком разных шрифтов, но все это будет один и тот же символ А.)
• Понятие графемы близко к глифу, но о графемах мы говорим в контексте языка, а не программного обеспечения. Графема может быть комбинацией (простой или не очень) двух и более глифов. Так пользователь воспринимает символ в контексте своего родного языка. Разница настолько тонкая, что большинство программистов могут о ней никогда не задумываться.
Что же тогда такое символ? Даже в мире Unicode нет четкого понимания этого предмета, поскольку языки ведут себя по-разному, а программисты мыслят иначе, чем прочие люди. Будем говорить, что символ — это абстракция написания знака, который визуально может быть представлен одним или несколькими способами.
Перейдем к конкретике. Сначала я хочу познакомить вас с нотацией. Традиционно кодовые позиции Unicode записываются как U+, а затем четыре или более шестнадцатеричных цифр в верхнем регистре. То, что мы называем латинской буквой А, можно представить в виде U+0041.
Теперь возьмем букву é (строчная е с акутом). Ее можно представить в Unicode двумя способами. Во-первых, это одна кодовая позиция U+00E9 (СТРОЧНАЯ ЛАТИНСКАЯ Е С АКУТОМ). С другой стороны, это сочетание двух кодовых позиций: строчная е + диакритический знак акут — U+0065 и U+0301. Иными словами, СТРОЧНАЯ ЛАТИНСКАЯ Е, за которой следует АКУТ.
Обе формы одинаково правильны. Более короткая называется монолитной (precomposed) формой. Однако имейте в виду, что не для каждого языка имеются монолитные варианты, поэтому не всегда можно свести подобный символ к одной кодовой позиции.
Я назвал Unicode кодировкой, но это не вполне верно. Unicode отображает символы на кодовые позиции; существуют разные способы отобразить кодовые позиции на двоичное представление. По существу, Unicode — это семейство кодировок.
Возьмем, к примеру, строку "Matz". Она состоит из четырех кодовых позиции Unicode:
# U+004d U+0061 U+0074 U+007a
Естественнее всего сохранить их в виде простой последовательности байтов.
00 4d 00 61 00 74 00 7а
Такая кодировка называется UCS-2 (два байта) или UTF-16 (16 битов). Отметим, что эта кодировка имеет две разновидности: тупоконечную (big-endian) и остроконечную (little-endian) — в зависимости от того, старший или младший байт хранится первым.
Заметим, однако, что каждый второй байт в этой последовательности нулевой. Это не просто совпадение, английский язык редко выходит за пределы кодовой позиции U+00FF. Так разбрасываться памятью расточительно.
И это наблюдение подводит нас к идее кодировки UTF-8. В ней «традиционные» символы представлены одним байтом, а остальные — несколькими. Вот как записывается та же строка в кодировке UTF-8:
4d 61 74 7а
Мы всего лишь избавились от нулей. Однако более важен тот факт, что мы получили обычную кодировку ASCII. Так и задумано: «простой ASCII» можно считать собственным подмножеством UTF-8.
Отсюда, в частности, следует, что при интерпретации текста в кодировке UTF-8 как ASCII-текста он выглядит «как обычно» (особенно если это преимущественно англоязычный текст). Иногда вы видите, что браузер или другое приложение отображает английский текст правильно, но местами появляются «крокозябры». Это, скорее всего, означает, что программа сделала неверные предположения об используемой кодировке.
Итак, можно сказать, что UTF-8 экономит память. Конечно, я снова становлюсь на англоцентрическую точку зрения (по крайней мере, ASCII-центрическую). Если текст в основном состоит из ASCII-символов, то да, память экономится, но для других языков, например греческого или русского, размер строк даже увеличится.
Еще одно очевидное достоинство UTF-8 — «обратная совместимость» с кодировкой ASCII, которая, по-видимому, все еще остается самой распространенной однобайтовой кодировкой в мире. Наконец, у UTF-8 есть некоторые особенности, делающие ее удобной для программистов.
Во-первых, байты, входящие в состав многобайтовых символов, тщательно подобраны. Нулевой байт (ASCII 0) никогда не встречается в качестве n-ого байта в последовательности (где n > 1); то же самое справедливо для таких распространенных символов, как косая черта (обычно используется для разделения компонентов пути к файлу). На самом деле никакой байт из диапазона 0x00-0x7F не может быть частью никакого другого символа.
Второй байт многобайтового символа однозначно определяет, сколько байтов за ним следует. Этот второй байт всегда выбирается из диапазона от 0хС0 до 0хF0, а следующие за ним — из диапазона от 0x80 до 0xBF. Таким образом, схема кодирования свободна от состояния и позволяет восстанавливать пропущенные или искаженные байты.
UTF-8 — одна из самых распространенных и гибких кодировок в мире. Она применяется с начала 1990-х годов и является кодировкой по умолчанию XML-документов. В этой главе мы будем иметь дело главным образом именно с UTF-8.
4.2. Кодировки в пост-ASCII мире
«Век ASCII» прошел, хотя не все еще осознали этот факт. Многие допущения, которые программисты делали в прошлом, уже несправедливы. Нам необходимо новое мышление.
Есть две идеи, которые, на мой взгляд, являются основополагающими, почти аксиомами. Во-первых, строка не имеет внутренней интерпретации. Она должна интерпретироваться в соответствии с некоторым внешним стандартом. Во-вторых, байт и символ — не одно и то же; символ может состоять из одного или нескольких байтов. Есть и другие уроки, но это самое важное.
Эти факты оказывают на программирование тонкое влияние. Рассмотрим сначала, как следует работать с символьными строками по-современному.
4.2.1. Библиотека jcode и переменная $KCODE
Чтобы использовать в Ruby разные наборы символов, вы должны знать о глобальной переменной $KCODE, от значения которой зависит поведение многих системных методов, манипулирующих строками. (Кстати говоря, буква K — напоминание о кандзи, одной из иероглифических азбук в японском языке.) Эта переменная принимает одно из пяти стандартных значений, каждое из которых представлено одной буквой, неважно — строчной или прописной (ASCII и NONE — одно и то же).
a ASCII
n NONE (ASCII)
е EUC
s SJIS
u UTF-8
Для ясности можно пользоваться и полными названиями (например, $KCODE =" UTF-8" ). Важен только первый символ.
О кодировке ASCII мы уже знаем. EUC и Shift-JIS (SJIS) нам малоинтересны. Мы сосредоточимся на значении UTF-8.
Установив значение $KCODE, вы задаром получаете весьма богатую функциональность. Например, метод inspect (он автоматически вызывается при обращении к методу p для печати объекта в читаемом виде) обычно учитывает текущее значение $KCODE.
$KCODE = "n"
# Для справки: французское слово "épée"
# обозначает разновидность меча (sword).
eacute = ""
eacute << 0303 << 0251 # U+00E9
sword = eacute + "p" + eacute + "e"
p eacute # "\303\251"
p sword # "\303\251p\303\251e"
$KCODE = "u"
p eacute # "é"
p sword # "épée"
Регулярные выражения в режиме UTF-8 тоже становятся несколько «умнее».
$KCODE = "n"
letters = sword.scan(/(.)/)
# [["\303"], ["\251"], ["p"], ["\303"], ["\251"], ["e"]]
puts letters.size # 6
$KCODE = "u"
letters = sword.scan(/(.)/)
# [["é"], ["p"], ["é"], ["e"]]
puts letters.size # 4
Библиотека jcode предоставляет также несколько полезных методов, например jlength и each_char. Рекомендую включать эту библиотеку с помощью директивы require всякий раз, как вы работаете с кодировкой UTF-8.
В следующем разделе мы снова рассмотрим некоторые типичные операции со строками и регулярными выражениями. Заодно поближе познакомимся с jcode.
4.2.2. Возвращаясь к строкам и регулярным выражениям
При работе с UTF-8 некоторые операции ничем не отличаются. Например, конкатенация строк выполняется так же, как и раньше:
"éр" + "éе" # "épée"
"éр" << "éе" # "épée"
Поскольку UTF-8 не имеет состояния, то для проверки вхождения подстроки тоже ничего специально делать не нужно:
"épée".include?("é") # true
Однако при написании интернациональной программы некоторые типичные допущения все же придется переосмыслить. Ясно, что символ больше не эквивалентен байту. При подсчете символов или байтов надо думать о том, что именно мы хотим сосчитать и для чего. То же относится к числу итераций.
По общепринятому соглашению, кодовую позицию часто представляют себе как «программистский символ». Это еще одна полуправда, но иногда она оказывается полезной.
Метод jlength возвращает число кодовых позиций в строке, а не байтов. Если нужно получить число байтов, пользуйтесь методом length.
$KCODE = "u"
require 'jcode'
sword = "épée"
sword.jlength # 4
sword.length # 6
Такие методы, как upcase и capitalize, обычно неправильно работают со специальными символами. Это ограничение текущей версии Ruby. (Не стоит считать ошибкой, поскольку получить представление слова с первой прописной буквой довольно трудно; такая задача просто не решается в схеме интернационализации Ruby. Считайте, что это нереализованное поведение.)
$KCODE = "u"
sword.upcase # "ÉPÉE"
sword.capitalize # "épée"
Если вы не пользуетесь монолитной формой, то в некоторых случаях метод может сработать, поскольку латинские буквы отделены от диакритических знаков. Но в общем случае работать не будет — в частности, для турецкого, немецкого, голландского и любого другого языка с нестандартными правилами преобразования регистра.
Возможно, вы думаете, что неакцентированные символы в некотором смысле эквивалентны своим акцентированным вариантам. Это почти всегда не так. Здесь мы имеем дело с разными символами. Убедимся в этом на примере метода count:
$KCODE = "u"
sword.count("e") # 1 (не 3)
Но для составных (не монолитных) символов верно прямо противоположное. В этом случае латинская буква распознается.
Метод count возвращает сбивающий с толку результат, когда ему передается многобайтовый символ. Метод jcount ведет себя в этом случае правильно:
$KCODE = "u"
sword.count("eé") # 5 (не 3)
sword.jcount("eé") # 3
Существует вспомогательный метод mbchar?, который определяет, есть ли в строке многобайтовые символы.
$KCODE = "u"
sword.mbchar? # 0 (смещение первого многобайтового символа)
"foo".mbchar? # nil
В библиотеке jcode переопределены также методы chop, delete, squeeze, succ, tr и tr_s. Применяя их в режиме UTF-8, помните, что вы работаете с версиями, «знающими о многобайтовости». При попытке манипулировать многобайтовыми строками без библиотеки jcode вы можете получить странные или ошибочные результаты.
Можно побайтно просматривать строку, как обычно, с помощью итератора each_byte. А можно просматривать посимвольно с помощью итератора each_char. Второй способ имеет дело с односимвольными строками, первый (в текущей версии Ruby) — с однобайтными целыми. Разумеется, мы в очередной раз приравниваем кодовую позицию к символу. Несмотря на название, метод each_char на самом деле перебирает кодовые позиции, а не символы.
$KCODE = "u"
sword.each_byte {|x| puts x } # Шесть строк с целыми числами.
sword.each_char {|x| puts x } # Четыре строки со строками.
Если вы запутались, не переживайте. Все мы через это проходили. Я попытался свести все вышесказанное в таблицу 4.1.
Таблица 4.1. Составные и монолитные формы
Монолитная форма "é" | ||||
Название символа | Глиф | Кодовая позиция | Байты UTF-8 | Примечания |
Строчная латинская e с акутом | é | U+00E9 | 0xC3 0хА9 | Один символ, одна кодовая позиция, один байт |
Составная форма "é" | ||||
Название символа | Глиф | Кодовая позиция | Байты UTF-8 | Примечания |
Строчная латинская е | е | U+0065 | 0x65 | Один символ, две кодовых позиции (два «программистских символа»), три байта UTF-8 |
Модифицирующий акут | ́ | U+0301 | 0xCC 0x81 |
Что еще надо учитывать при работе с интернациональными строками? Квадратные скобки по-прежнему относятся к байтам, а не к символам. Но при желании это можно изменить. Ниже приведена одна из возможных реализаций (не особенно эффективная, зато понятная):
class String
def [](index)
self.scan(/./)[index]
end
def []=(index,value)
arr = self.scan(/./)
arr[index] = value
self.replace(arr.join)
value
end
end
Конечно, здесь не реализована значительная часть функциональности настоящего метода [], который понимает диапазоны, регулярные выражения и т.д. Если вам все это нужно, придется запрограммировать самостоятельно.
У метода unpack есть параметры, помогающие манипулировать Unicode-строками. Указав в форматной строке параметр U*, мы можем преобразовать строку в кодировке UTF-8 в массив кодовых позиций (U без звездочки преобразует только первую кодовую позицию):
codepoints = sword.unpack('U*') # [233, 112, 233, 101]
Вот несколько более полезный пример, в котором все кодовые позиции в строке, отличные от ASCII (то есть начиная с U+0080), преобразуются к виду U+XXXX, который мы обсуждали выше:
def reveal_non_ascii(str)
str.unpack('U*').map do |cp|
if cp < 0x80
cp.chr
else
'(U+%04X)' % cp
end
end.join
end
У метода String#unpack есть «близкий родственник» Array#pack, выполняющий обратную операцию:
[233, 112, 233, 101].pack('U*') # "épée"
Мы можем воспользоваться им, чтобы вставить Unicode-символы, которые трудно ввести с клавиатуры:
eacute = [0хЕ9].pack('U')
cafe = "caf#{eacute}" # "café"
Регулярным выражениям тоже известно о многобайтовых символах, особенно если вы пользуетесь библиотекой Oniguruma (мы рассматривали ее в главе 3). Например, образец /./ сопоставляется с одним многобайтовым символом.
Модификатор u извещает регулярное выражение о том, что мы работаем с кодировкой UTF-8. Если $KCODE равно "u", то модификатор можно не задавать, однако это и не повредит. (К тому же такая избыточность может быть полезна, если код является частью большой программы, а какое значение переменной $KCODE в ней установлено, вам неизвестно.)
Даже без Oniguruma регулярные выражения распознают, относится ли данный многобайтовый символ к категории тех, что могут входить в состав слова:
$KCODE = "u"
sword =~ /\w/ #0
sword =~ /\W/ # nil
При наличии Oniguruma последовательности, начинающиеся с символа обратной косой черты (\w, \s и т.п.) распознают и более широкие диапазоны кодовых точек: слова, пропуски и т.д.
Регулярные выражения позволяют безопасно выполнять простые манипуляции со строками. Мы и так можем без труда усекать строки. Следующий код возвращает не более 20 символов из строки ascii_string:
ascii_string[0,20]
Однако, поскольку кодовая позиция Unicode может занимать более одного байта такую технику нельзя безопасно применять к строке в кодировке UTF-8. Есть риск, что в конце строки окажется недопустимая последовательность байтов. Кроме того, это не слишком полезно, так как мы не можем заранее сказать, сколько в результате получится кодовых позиций. На помощь приходят регулярные выражения:
def truncate(str, max_length)
str[/.{0,#{max_length}}/m]
end
4.2.3. Распознавание кодировки
Распознать, в какой кодировке записана данная строка, довольно сложно. Многобайтовые кодировки обладают отличительными признаками, по которым их можно опознать, но с однобайтовыми — а именно они применяются в западных языках — дело обстоит куда хуже. Для решения можно применить статистические методы, но эта тема выходит за рамки данной книги (к тому же результат в общем случае получается не слишком надежным).
К счастью, обычно перед нами стоит более простая задача — выяснить, записана ли строка в кодировке UTF-8. На этот вопрос можно дать достаточно надёжный ответ. Приведем один способ (основанный на том, что метод unpack возбуждает исключение, если ему передана некорректная строка):
class String
def utf8?
unpack('U*') rescue return false
true
end
end
4.2.4. Нормализация Unicode-строк
До сих пор мы пользовались монолитными символами, в которых базовый символ и диакритический знак объединены в одну кодовую позицию. Но, вообще говоря, в Unicode символы и диакритические знаки представлены отдельно. Вместо того чтобы хранить букву é в кодовой позиции СТРОЧНАЯ ЛАТИНСКАЯ БУКВА E С АКУТОМ, можно было бы представить ее в составной форме как СТРОЧНУЮ ЛАТИНСКУЮ БУКВУ E и МОДИФИЦИРУЮЩИЙ АКУТ.
Для чего это может понадобиться? Для обеспечения дополнительной гибкости и возможности применять диакритические знаки к любому символу, а не ограничивать себя комбинациями, которые предусмотрел проектировщик кодировки. На самом деле в шрифты включены глифы для наиболее распространенных комбинаций символа и диакритического знака, но отображение символа и его кодирование — вещи разные.
При проектировании Unicode приходилось учитывать такие вещи, как эффективность и совместимость с существующими национальными кодировками. Иногда это приводит к избыточности; например, в Unicode имеются кодовые позиции как для составных форм, так и для многих уже применяющихся монолитных форм.
Рассмотрим, к примеру, немецкое слово «öffnen» (открывать). Даже если забыть о регистре, его можно закодировать четырьмя способами:
1. о + МОДИФИЦИРУЮЩАЯ ТРЕМА (u+0308) +f+f+n+e+n
2. СТРОЧНАЯ ЛАТИНСКАЯ БУКВА О С ТРЕМОЙ (U+00F6) + f + f + n + е + n
3. о + МОДИФИЦИРУЮЩАЯ ТРЕМА + ЛИГАТУРА ДВОЙНОЕ F (U+FB00) + n + е + n.
4. СТРОЧНАЯ ЛАТИНСКАЯ БУКВА О С ТРЕМОЙ + ЛИГАТУРА ДВОЙНОЕ F + n + e + n
Трема — это две точки над буквой (в немецком языке называется «умляут»).
Нормализацией называется процедура приведения разных представлений символа к стандартной форме. Можно быть уверенным, что после нормализации данный символ закодирован вполне определенным образом. Каким именно, зависит оттого, чего мы хотим достичь. В приложении 15 к стандарту Unicode перечислены четыре формы нормализации:
1. Форма D (каноническая декомпозиция).
2. Форма С (каноническая декомпозиция с последующей канонической композицией).
3. Форма KD (совместимая декомпозиция).
4. Форма KC (совместимая декомпозиция с последующей канонической композицией).
Иногда можно встретить аббревиатуры NKFC (Normalization Form KC) и т.д.
Точные правила, сформулированные в стандарте, довольно сложны; в них проведено различие между «канонической эквивалентностью» и «совместимой эквивалентностью». (Корейский и японский языки требуют особого рассмотрения, но мы не станем тратить на это время.) В таблице 4.2 показано, как форма нормализации влияет на приведенные выше строки.
Таблица 4.2. Нормализованные формы в Unicode
Исходная | NFD | NFC | NFKD | NFKC |
o+ ̈+f+f+n+e+n | o+ ̈+f+f+n+e+n | ö+f+f+n+e+n | o+ ̈+f+f+n+e+n | ö+f+f+n+e+n |
ö+f+f+n+e+n | o+ ̈+f+f+n+e+n | ö+f+f+n+e+n | o+ ̈+f+f+n+e+n | ö+f+f+n+e+n |
o+ ̈+ff+n+e+n | o+ ̈+ff+n+e+n | ö+ff+n+e+n | o+ ̈+f+f+n+e+n | ö+f+f+n+e+n |
ö+ff+n+e+n | o+ ̈+ff+n+e+n | ö+ff+n+e+n | o+ ̈+f+f+n+e+n | ö+f+f+n+e+n |
Формы С и D обратимы, KC и KD — нет. С другой стороны, потеря некоторых данных в формах KC и KD — свидетельство того, что все четыре строки двоично эквивалентны. Какая форма лучше всего подходит, зависит от приложения. Мы ещё вернемся к этой теме в следующем разделе.
Для Ruby есть библиотека, позволяющая выполнить описанные нормализации, хотя в стандартный дистрибутив она не входит. Вы можете скачать ее со страницы http://www.yoshidam.net/Ruby.html и установить командой gem install Unicode.
Если библиотека Unicode установлена, то для выполнения любой нормализации достаточно вызвать один из методов Unicode.normalize_x:
require 'Unicode'
sword_kd = Unicode.normalize_KD(sword)
sword_kd.scan(/./) # ["e", "'", "p", "e", "'", "e"]
sword_kc = Unicode.normalize_KC(sword)
sword_kc.scan(/./) # [ "é", "p", "é", "e"]
4.2.5. Упорядочение строк
Обычно, хотя и не всегда, строки упорядочиваются по алфавиту или сходным образом. Упорядочение тесно связано с нормализацией: в обоих случаях применяются одни и те же идеи и библиотеки.
Предположим, например, что мы хотим отсортировать такой массив строк:
eacute = [0x00Е9].pack('U')
acute = [0x0301].pack('U')
array = ["epicurian", "#{eacute}p#{eacute}e", "e#{acute}lan"]
# ["epicurian", "éрéе", "élan"]
Что произойдет, если передать этот массив методу Array#sort?
array.sort # ["epicurian", "élan", "éрéе"]
He годится!.. Попытаемся понять, почему так получилось. Сортируемые строки Ruby сравнивает побайтно. Чтобы убедиться в этом, достаточно взглянуть на первые несколько байтов каждой строки:
array.map {|item| "#{item}: #{item.unpack('С*')[0,3].join(',')}" }
# ["epicurian: 101,112,105", "éрéе: 195,169,112",
# "élan: 101,204,129"]
Тут возникают две трудности. Во-первых, символы UTF-8, не имеющие аналога в кодировке ASCII, начинаются с байта, имеющего большое числовое значение, а стало быть, после сортировки неизбежно окажутся после ASCII-символов. Во-вторых, составные латинские символы оказываются раньше монолитных из-за первого ASCII-байта.
В системные библиотеки обычно включают функции сортировки, которые сравнивают строки в соответствии с правилами конкретного языка. В библиотеке, поставляемой вместе с компилятором языка С, для этого служат функции strxfrm и strcoll.
Имейте в виду, что проблема возникает даже в случае кодировки ASCII. При сортировке ASCII-строк в Ruby производится прямое лексикографическое сравнение, однако в реальной жизни (например, если мы хотим отсортировать по названиям книги из библиотеки Конгресса США) есть много правил, которые не учитываются при таком упрощенном подходе.
Для упорядочения строк можно создать промежуточные строки и отсортировать именно их. Как конкретно это сделать, зависит от предъявляемых требований и языка; универсального алгоритма не существует.
Предположим, что список обрабатывается согласно правилам английского языка, причем диакритические знаки игнорируются. Первым делом нужно определить методику трансформации. Мы приведем все символы к составному виду, а затем исключим диакритические знаки, оставив только базовые символы. Для модифицирующих диакритических знаков в Unicode выделен диапазон от U+0300 to U+036F:
def transform(str)
Unicode.normalize_KD(str).unpack('U*').select{ |cp|
cp < 0x0300 || cp > 0x036F
}.pack('U*')
end
array.map{|x| transform(x) } # ["epicurian", "epee", "elan"]
Затем создадим хэшированную таблицу, чтобы установить соответствие между исходными и трансформированными строками, и воспользуемся ей для сортировки исходных строк. Наличие такой таблицы позволяет провести трансформацию только один раз.
def collate(array)
transformations = array.inject({}) do |hash, item|
hash[item] = yield item
hash
end
array.sort_by {|x| transformations[x] }
end
collate(array) {|a| transform(a) } # ["élan", "épée", "epicurian"]
Уже лучше, но мы еще не учли прописные буквы и эквивалентность символов. Возьмем для примера немецкий язык.
На самом деле в немецком языке есть несколько способов упорядочения; мы остановимся на стандарте DIN-2 (как в телефонном справочнике). Согласно этому стандарту, символ ß (эсцет) эквивалентен ss, а умляут эквивалентен букве е (то есть ö — то же самое, что ое и т.д.).
Наш метод трансформации должен учитывать эти детали. Снова начнем с декомпозиции составных символов. Например, модифицирующая трема (умляут) представляется кодовой позицией U+0308. За основу мы возьмем метод преобразования регистра, имеющийся в Ruby, но несколько дополним его. Вот как выглядит теперь код трансформации:
def transform_de(str)
decomposed = Unicode.normalize_KD(str).downcase
decomposed.gsub!('ß', 'ss')
decomposed.gsub([0x0308].pack('U'), 'e')
end
array = ["Straße", "öffnen"]
array.map {|x| transform_de(x) } # ["strasse", "oeffnen"]
He для всех языков годится такой прямолинейный подход. Например, в испанском между буквами n и о есть еще буква ñ. Однако, если каким-то образом сдвинуть оставшиеся буквы, то мы справимся и с этой проблемой. В листинге 4.1 для упрощения обработки нормализация применена к монолитным символам. Кроме того, мы облегчили себе жизнь, игнорируя различия между буквами с диакритическими знаками и без них.
Листинг 4.1. Упорядочение строк в испанском языке
def map_table(list)
table = {}
list.each_with_index do |item, i|
item.split(',').each do |subitem|
table[Unicode, normalize_KC(subitem)] = (?a + i).chr
end
end
table
end
ES_SORT = map_table(%w(
a,A,á,Á b,B c,C d,D е,Е,é,É f,F g,G h,H i,I,í,Í j,J k,K l,L m,M
n,N ñ,Ñ o,O,ó,Ó p,P q,Q r,R s,S t,T u,U,u,U v,V w,W x,X y,Y z,Z
))
def transform_es(str)
array = Unicode.normalize_KC(str).scan(/./u)
array.map {|c| ES_SORT[c] || c}.join
end
array = %w[éste estoy año apogeo amor]
array.map {|a| transform_es(a) }
# ["etue", "etupz", "aop", "aqpgep", "amps"]
collate(array) {|a| transform_es(a) }
# ["amor", "año", "apogeo", "éste", "estoy"]
В реальности упорядочение немного сложнее, чем показано в примерах выше; обычно требуется до трех уровней обработки. На первом уровне сравниваются только базовые символы без учета диакритических знаков и регистра, на втором учитываются диакритические знаки, а на третьем — регистр. Второй и третий уровень необходимы лишь в том случае, когда на предыдущих уровнях строки совпали. Кроме того, в некоторых языках последовательности, состоящие из нескольких символов, сортируются как единая семантическая единица (например, в хорватском lj расположено между l и m). Поэтому разработка языковозависимого или обобщенного алгоритма сортировки — задача нетривиальная: необходимо хорошо разбираться в конкретном языке. Невозможно изобрести по-настоящему универсальный алгоритм сортировки, который давал бы правильные результаты для всех языков, хотя попытки в этом направлении производились.
4.2.6. Преобразование из одной кодировки в другую
В стандартной библиотеке Ruby имеется интерфейс к библиотеке iconv для преобразования из одной кодировки символов в другую. Она должна работать на всех платформах, в том числе и в Windows (если дистрибутив устанавливался моментальным инсталлятором).
Чтобы преобразовать строку из UTF-8 в ISO-8859-15, библиотека iconv используется следующим образом:
require 'iconv'
converter = Iconv.new('ISO-8859-15', 'UTF-8')
sword_iso = converter.iconv(sword)
Важно помнить, что сначала указывается целевая кодировка, а потом исходная (как при присваивании). Количество и названия поддерживаемых кодировок зависят от платформы, но наиболее распространенные стандартизованы и имеются везде. Если установлена пакетная утилита iconv, то перечень распознаваемых кодировок можно получить с помощью команды iconv -l.
Помимо названия кодировки, iconv принимает еще флаги, управляющие ее поведением. Они указываются в конце строки, содержащей целевую кодировку.
Обычно iconv возбуждает исключение, если получает недопустимые входные данные или почему-либо не может представить их в целевой кодировке. Флаг //IGNORE подавляет исключение.
broken_utf8_string = "hello\xfe"
converter = Iconv.new('ISO-8859-15', 'UTF-8')
# будет возбуждено исключение Iconv::IllegalSequence
converter.iconv(broken_utf8_string)
converter = Iconv.new('ISO-8859-15//IGNORE', 'UTF-8')
converter.iconv(broken_utf8_string) # "hello"
Этот же флаг позволяет очистить строку от неверных данных:
broken_sword = "épée\xfe"
converter = Iconv.new('UTF-8//IGNORE', 'UTF-8')
converter.iconv(broken_sword) # "épée"
Иногда некоторые символы нельзя представить в целевой кодировке. Обычно в этом случае возбуждается исключение. Флаг //TRANSLIT говорит iconv, что нужно вместо этого попытаться подобрать приблизительные эквиваленты.
converter = Iconv.new('ASCII', 'UTF-8')
converter.iconv(sword) # Возбуждается Iconv::IllegalSequence.
converter = Iconv.new('ASCII//IGNORE', 'UTF-8')
converter.iconv(sword) # "pe"
converter = Iconv.new('ASCII//TRANSLIT', 'UTF-8')
converter.iconv(sword) # "'ep'ee"
Этим свойством можно воспользоваться, чтобы получить URL, содержащий только ASCII-символы:
str = "Straße épée"
converter = Iconv.new('ASCII//TRANSLIT', 'UTF-8')
converter.iconv(sword).gsub(/ /, '-').gsub(/[^а-z\-]/in).downcase
# "strasse-epee"
Однако работать это будет лишь в отношении латиницы. В листинге 4.2 приведен реальный пример совместного применения библиотек iconv и open-uri для скачивания Web-страницы и перекодирования ее в UTF-8.
Листинг 4.2. Перекодирование Web-страницы в кодировку UTF-8
require 'open-uri'
require 'iconv'
def get_web_page_as_utf8(url)
open(url) do |io|
source = io.read
type, *parameters = io.content_type_parse
# He перекодировать, если не (X)HTML
unless type =~ %r!^(?:text/html|application/xhtml+xml)$!
return source
end
# Сначала проверяем заголовки, присланные сервером:
if pair = parameters.assoc('charset')
encoding = pair.last
# Затем анализируем HTML:
elsif source =~ ?\]*?charset=([^\s'"]+)/i
encoding = $1
# Если не удалось определить, предполагаем кодировку по умолчанию,
# определенную в стандарте HTTP.
else
encoding = 'ISO-8859-1'
end
converter = Iconv.new('UTF-8//IGNORE', encoding)
return converter.iconv(source)
end
end
Это еще не все системные вопросы, связанные с преобразованием кодировок. Предположим, что в операционной системе, где установлен Ruby, определена локаль, отличная от UTF-8, или Ruby общается с ОС не в UTF-8 (так, например, обстоит дело в дистрибутиве для Win32). Тогда возникают дополнительные сложности.
Например, Windows поддерживает Unicode в именах файлов и на системном уровне работает исключительно в Unicode. Но в настоящее время Ruby взаимодействует с Windows при помощи устаревших кодовых страниц. Для англоязычного и большинства других западных изданий это страница 1252 (или WINDOWS-1252).
Внутри программы можно пользоваться и кодировкой UTF-8, но все имена файлов придется перевести в кодировку, заданную кодовой страницей. Iconv поможет это сделать, но важно не забывать, что кодовая страница позволяет описать только малое подмножество всех символов, имеющихся в Unicode.
Кроме того, это означает, что пока Ruby для Windows не может открывать файлы, имена которых нельзя описать с помощью кодовой страницы. Это ограничение не относится к Mac OS X, Linux и другим системам с локалью UTF-8.
4.3. Справочники сообщений
Справочник сообщений — это набор сообщений на одном языке. Данное понятие неотъемлемо от концепции локализации (L10N). Идея в том, чтобы отделить языково-зависимые строки от остальной программы. Тогда для того, чтобы программа «заговорила» на другом языке, достаточно всего лишь подменить справочник.
«Наилучший» способ реализовать эту идею в Ruby — воспользоваться библиотекой Ruby-GetText-Package. Я буду называть ее просто gettext, поскольку именно так называется содержащий ее файл (не путайте с утилитой gettext!). Эту великолепную библиотеку написал Macao Муто (Masao Mutoh), он же очень помог при написании данного раздела.
Библиотека представляет собой реализацию на Ruby (не обертку) набора утилит gettext из проекта GNU (самый известный продукт в этой области). Ее официальный сайт находится по адресу http://gettext.rubyforge.org/, а утилиты GNU можно найти на сайте http://www.gnu.org/software/gettext/.
4.3.1 Исторические сведения и терминология
Библиотека gettext на самом деле, как мы увидим, состоит из нескольких библиотек. Для доступа к основным функциям нужно включить предложение require 'gettext', а для получения разного рода дополнительных средств (в частности, работы со справочниками сообщений) — предложение require 'gettext/utils'.
Главная причина, по которой мы используем справочники сообщений, — это, конечно, перевод сообщений на другие языки. С их помощью мы также обрабатываем случаи, когда формы единственного и множественного числа различаются (один файл, два файла). Кстати, эти правила очень сильно зависят от конкретного языка.
Обычно у каждой библиотеки и приложения имеется собственный справочник сообщений. Следовательно, в дистрибутив можно включать набор переведенных на разные языки справочников.
Учитываются переменные окружения LANG и GETTEXT_PATH. Их назначение мы рассмотрим ниже.
Для сопровождения справочника сообщений есть две основных операции (они выполняются вне вашей программы): извлечь сообщения из исходного текста Ruby-программы для формирования начального справочника и включить новые сообщения из исходного текста в существующий справочник (слияние). Операции извлечения и слияния мы рассмотрим в разделе 4.3.3.
4.3.2. Приступаем к работе со справочниками сообщений
Возможно, библиотека gettext на вашем компьютере уже установлена. Если нет, проще всего выполнить команду gem install gettext.
Для разработки вам понадобятся утилиты GNU. Если вы работаете в системе UNIX, то, скорее всего, они уже установлены. В случае платформы Win32 можно установить Glade/GTK+ для Windows; заодно вы получите и утилиты GNU. В любом случае необходимы они только на этапе разработки, а не во время выполнения.
Если у вас нет программы rake, установите ее из gem-пакета. Это дополнительное удобство.
Коль скоро среда настроена и все установлено, можно приступать к работе со справочниками. Но сначала познакомимся с терминологией.
• РО-файл — это переносимый объектный файл. Так называется текстовое (понятное человеку) представление справочника сообщений. У каждого такого файла есть вариант для различных поддерживаемых локалей. РОТ-файл — это шаблон.
• МО-файл — это переносимый двоичный файл справочника. Он создается из РО-файла. Библиотека для Ruby умеет читать только МО-файлы, но не РО-файлы.
• Текстовый домен — это, по существу, просто базовое имя МО-файла. Он ассоциирован с приложением (привязан к нему).
4.3.3. Локализация простого приложения
В следующем примере определяется класс Person, после чего с ним выполняются различные действия. Метод show выводит локализованные сообщения:
require 'gettext'
class Person
include GetText
def initialize(name, age, children_num)
@name, @age, @children_num = name, age, children_num
bindtextdomain("myapp")
end
def show
puts _("Information")
puts _("Name: %{name}, Age: %{age}") % {:name => @name, :age => @age}
puts n_("%{name} has a child.", "%{name} has %{num} children.",
@children_num) % {:name => @name, :num => @children_num}
end
end
john = Person.new("John", 25, 1)
john.show
linda = Person.new("Linda", 30, 3)
linda.show
Предположим, что этот код сохранен в файле myapp/person.rb. Как вы скоро увидите, иерархия каталогов имеет значение. Вызов метода bindtextdomain связывает текстовый домен "myapp" с объектом Person во время выполнения.
В методе show есть три обращения к библиотеке gettext. Вызываемый метод называется _ (одно подчеркивание), чтобы не отвлекать внимание.
Первое обращение просто выводит локализованное сообщение, соответствующее строке "Information". Второе демонстрирует локализованное сообщение с двумя параметрами. В хэше задается список значений, подставляемых в строку. Интерполировать их напрямую нельзя, потому что это вступало бы в противоречие с основной целью: хранить в справочнике небольшое число сообщений.
Отметим также, что параметры отделены от текста сообщения, поэтому при необходимости могут подставляться в другом порядке. Ведь иногда при переводе на другой язык приходится переставлять слова.
Тот же метод можно вызвать и короче:
puts _("Name: %s, Age: %d") % [@name, @age]
Однако мы рекомендуем более длинную запись. Она понятнее и дает больше информации переводчику.
Метод n_ предназначен для обработки единственного и множественного числа. Значение параметра @children_num — индекс, говорящий о том, какую из заранее заданных строк использовать. (Правило Plural-Forms, о котором я скоро расскажу, определяет порядок вычисления индекса.)
Отметим, что сообщения по умолчанию обязаны быть англоязычными (даже если родной язык программиста не английский). Нравится вам это или нет, но английский ближе всего к универсальному языку с точки зрения большинства переводчиков.
Я сказал, что нам пригодится программа rake. Создадим файл Rakefile (в каталоге myapp) для сопровождения справочников сообщений. Он будет выполнять две основные операции: обновлять РО-файлы и создавать МО-файлы.
require 'gettext/utils'
desc "Update pot/po files."
task :updatepo do
GetText.update_pofiles("myapp", ["person.rb"], "myapp 1.0.0")
end
desc "Create mo-files"
task :makemo do
GetText.create_mofiles
end
Здесь мы воспользовались библиотекой gettext/utils, в которой имеются функции для работы со справочниками сообщения. Метод update_pofiles создает начальный файл myapp/ро/myapp.pot на основе исходного текста person.rb. При втором (и всех последующих) вызовах эта функция выполнит обновление, или слияние файла myapp/po/myapp.pot и всех файлов вида myapp/po/#{lang}/myapp.ро. Второй параметр — массив целевых файлов. Обычно он задается примерно так:
GetText.update_pofiles("myapp",
Dir.glob("{lib,bin}/**/*.{rb,rhtml}"),
"myapp 1.0.0")
Вызов метода GetText.create_mofiles создает необходимые подкаталоги в каталоге data/locale/ и генерирует МО-файлы из РО-файлов.
Итак, выполнив команду rake updatepo, мы создадим каталог myapp/ро, а в нем файл myapp.pot.
Теперь отредактируем заголовок файла po/myapp.pot. Он содержит описание приложения (название, имя автора, адрес электронной почты, условия лицензирования и т.д.).
# Пример приложения. (Осмысленное название)
# Copyright (С) 2006 Foo Bar (Автор приложения)
# Файл распространяется по лицензии XXX. (Лицензия)
#
# FIRST AUTHOR
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: myapp 1.0.0\n" (ID и версия проекта)
#...
Что такое маркер fuzzy? Так отмечается тот факт, что какая-то часть не переведена или перевод вызывает сомнения. Все автоматически сгенерированные сообщения помечаются таким образом, чтобы человек знал, что их нужно проверить и изменить.
Файл myapp.pot нужно разослать переводчикам. (Конечно, вы можете перевести его и самостоятельно.)
Предположим, что вы переводите на японский язык. На машине установлена локаль ja_jp.UTF-8, что означает «Япония (ja), японский язык (JP), кодировка UTF-8».
Для начала скопируем файл myapp.pot в myapp.ро. При наличии набора GNU-утилит gettext лучше воспользоваться командой msginit, а не просто cp. Эта утилита учитывает переменные окружения и правильно устанавливает некоторые переменные в заголовке. В UNIX она вызывается следующим образом:
LANG=ja_JP.UTF-8 msginit -i myapp.pot -o myapp.po
Затем отредактируйте файл myapp.ро, как показано в листинге 4.3. Редактировать необходимо в той кодировке, которая указана в строке Content-Type.
Листинг 4.3. Файл myapp.ро после редактирования
# Пример приложения.
# Copyright (С) 2006 Foo Bar
# Файл распространяется по лицензии XXX.
#
# Ваше имя
# (Удалите строку 'fuzzy')
msgid ""
msgstr ""
"Project-Id-Version: myapp 1.0.0\n"
"POT-Creation-Date: 2006-05-22 23:27+0900\n"
"PO-Revision-Date: 2006-05-23 14:39+0900\n"
# Информация о текущем переводчике.
"Last-Translator: Your Name
"Language-Team: Japanese\n" (Ваш язык)
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" (Кодировка файла)
"Content-Transfer-Encoding: 8bit \r."
# Форма множественного числа.
"Plural-Forms: nplurals=2; plural=(n != l);\n"
#: person.rb:12
msgid "Information"
msgstr "Jouhou"
#: person.rb:13
msgid "Name: %{name}, Age: %{age}"
msgstr "Namae: %{name}, Nenrei: %{age}"
#: person.rb:14
msgid "%{name} has a child."
msgid_plural "%{name} has %{num} children."
msgstr[0] "%{name} ha hitori kodomo ga imasu."
msgstr[1] "%{name) ha %{num} nin no kodomo ga imasu."
Тегом msgid помечается исходное сообщение, а тегом msgstr — переведенное. При наличии строки msgid_plural необходимо включить отдельные строки msgstr[i] в соответствии с правилом Plural-Forms. Индекс i вычисляется на основе выражения Plural-Forms. В данном случае при num != 1 используется msgstr[1] (сообщение с существительным во множественном числе).
Истоки синтаксиса правила Plural-Forms следует искать в языке С. Как видим, он опирается на тот факт, что булевские выражения в С возвращают 0 или 1.
Имейте в виду, что формы единственного и множественного числа в большой степени зависят от языка. Во многих языках есть несколько форм множественного числа. Например, в польском слово «файл» в единственном числе записывается как «plik». Если количество экземпляров заканчивается на 2, 3 и 4, то во множественном числе пишется «pliki», а во всех остальных случаях — «plików».
Поэтому для польского языка правило Plural-Forms выглядит так:
Plural-Forms: nplurals=3; \
plural=n==1 ? 0 : \
n%10>=2 && n%10<=4 && (n%100=20) ? 1 : 2;
Заголовок файла — не пустая формальность. Особенно важны разделы Content-Type и Plural-Forms. При пользовании утилитой msginit они вставляются автоматически, в противном случае необходимо добавить их вручную.
Закончив работу, переводчик посылает файлы обратно разработчику (или вы сами возвращаетесь к роли разработчика).
Файлы myapp.ро, полученные от переводчиков, помещаются в соответствующие каталоги (внутри каталога myapp/ро). Например, французскую версию следовало бы поместить в каталог myapp/ро/fr/myapp.ро, немецкую — в каталог myapp/po/de/myapp.ро и т.д.
Затем выполните команду rake makemo. Она преобразует РО-файлы в МО-файлы. Сгенерированные МО-файлы будут помещены в каталог myapp/data/locale/ (в котором есть подкаталоги для каждого языка).
Таким образом, окончательная структура дерева каталогов выглядит так:
myapp/
Rakefile
person.rb
ро/
myapp.pot
de/myapp.po
fr/myapp.po
ja/myapp.po
:
data/
locale/
de/LC_MESSAGES/myapp.mo
fr/LC_MESSAGES/myapp.mo
ja/LC_MESSAGES/myapp.mo
:
Перевод закончен, можно протестировать пример. Но предварительно следует указать, где искать МО-файлы и для какой локали проводится тестирование. Установим переменные окружения GETTEXT_PATH и LANG, запустим программу и посмотрим, что она выведет.
export GETTEXT_PATH="data/locale"
export LANG="ja_JP.UTF-8"
ruby person.rb
Программа выводит локализованные сообщения в соответствии со значением переменной lang.
4.3.4. Прочие замечания
Если вы распространяете вместе со своей программой справочники сообщений, то лучше собрать пакет с помощью системы RubyGems или библиотеки setup.rb. Дополнительную информацию по этому поводу вы найдете в разделе 17.2.
При установке пакета, собранного RubyGems, справочники сообщений копируются в каталоги вида:
(gem-packages-installed-dir)/myapp-x.x.x/data/locale/
Такие каталоги уже включены в путь поиска для библиотеки gettext, поэтому ваша программа будет локализована даже без явной установки переменной окружения GETTEXT_PATH.
В случае сборки пакета с помощью библиотеки setup.rb справочники сообщений помещаются в каталог (system-dir)/share/locale/. И в этом случае локализация достигается без установки переменной GETTEXT_PATH.
Напомним, что описанная библиотека не является оберткой набора утилит gettext от GNU. Однако файлы сообщений совместимы, поэтому при желании вы можете пользоваться средствами сопровождения GNU. Понятно, что во время выполнения программы все эти инструменты не нужны, то есть пользователь не обязан их устанавливать на свой компьютер.
4.4. Заключение
В этой главе мы рассмотрели один из самых сложных аспектов программирования — проблему интернационализации кода. При этом нам понадобился материал из двух предыдущих глав, так как интернационализация тесно связана со строками и регулярными выражениями.
Мы видели, что в Ruby некоторые задачи решаются просто благодаря наличию библиотеки jcode и сопутствующих инструментов. Заодно мы познакомились с наборами символов вообще и с набором Unicode в частности.
Мы узнали, что регулярные выражения в общем случае лучше поддерживают Unicode, чем средства работы со строками, а также рассмотрели методы pack и unpack с точки зрения полезности для манипулирования Unicode-строками.
Наконец, мы довольно подробно остановились на справочниках сообщений. Мы поняли, для чего они нужны, как их создавать и поддерживать.
Детально рассмотрев вопрос о строках и регулярных выражениях, вернемся на главную дорогу. Глава 5 посвящена численному анализу в языке Ruby.