Последнее изменение: 1 сентября 2007г.
Вавилонское столпотворение. Часть 2. Локализация и ресурсы
Это вторая статья из цикла "Вавилонское столпотворение", посвященного локализации приложений. В ней пойдет речь о, собственно, процессе локализации, использовании ресурсов и тонкостях, существующих в этой области.
Мы поговорим о следующих темах:
- I18N vs. L10N – определение локализации
- Типы пакетов ресурсов
- Порядок поиска ресурсов
- Загрузка ресурсов из файлов свойств
Итак. Начнем с основ –
I18N vs. L10N – определение локализации
Собственно, в переводе приложения на другой язык различают ДВА процесса. Один из них – написание приложения так, чтобы
перевод не был проблематичным. Это означает, например, отказ от использования непосредственно в коде сообщений, форматов и
т.п. – всего, что может зависеть от языка. Этот процесс называется internationalization, сокращается это слово как
i18n
(по причине того, что между "i" и "n" в этом длинном слове 18 букв). Второй процесс – создание набора
ресурсов для полного перевода приложения на другой язык. Этот процесс зовется localization, и сокращается как
l10n
.
Так получилось, что оба процесса в русском языке называются одинаково. И очень часто между ними не делается никакой
разницы. В этой статье я рассмотрю оба, хотя большее внимание будет уделено первому – i18n
.
Итак, интернационализация. Господи, слово-то какое... :) В Java принят следующий подход. Существует понятие т.н.
"географического региона" – locale. Оно представлено в виде объекта типа java.util.Locale
и
характеризуется языком, страной и т.н. вариантом (разновидностью) языка. Именно в таком порядке, язык имеет приоритет над
страной. Это несколько странно в свете названия locale (дословно – "место действия"), но позволяет использовать
эти объекты для идентификации языка. Что весьма удобно при переводе приложений.
Далее. Ресурсы сосредоточены в т.н. ResourceBundle
. Существует механизм поиска ResourceBundle
,
соответствующего необходимому языку (здесь и далее я буду употреблять язык для обозначения locale, ибо
лучшего эквивалента найти не могу). Пакеты (bundle) ресурсов определяются по имени. Ресурсы в них – по
строковым ключам.
Таким образом, последовательность действий для нахождения ресурса, соответствующего языку, такова – найти
ResourceBundle
, соответствующий данному языку, после чего получить из него ресурс, соответствующий нужному
ключу. В дальнейшем мы рассмотрим оба процесса более подробно.
Ресурсы бывают разных типов. У каждого из них свои преимущества и недостатки. Однако механизм поиска при этом не меняется. В следующей части мы рассмотрим ...
Типы пакетов ресурсов
Для начала обратимся к базовому классу – java.util.ResourceBundle
. Он довольно прост. Этот класс позволяет
получить список ключей, объект по ключу, строку по ключу, массив строк по ключу. Собственно, его сервисные функции на этом
кончаются. Вряд ли вы будете использовать этот класс напрямую, во всяком случае на первых порах.
Есть, однако, в этом классе методы, которые составляют основу механизма поиска. Это три статических метода
getBundle
. Вы всегда должны задать имя пакета, можете задать язык (если не зададите – будет выбран язык по
умолчанию) и загрузчик классов. Я допускаю, что иногда его хочется задавать, но сам я с такими ситуациями не встречался.
Все, что касается поиска, мы обсудим в следующем разделе.
Вернемся к нашим... гм... пакетам. У класса java.util.ResourceBundle
есть два дочерних –
java.util.ListResourceBundle
и java.util.PropertyResourceBundle
. Первый из них – абстрактный, второй нет.
ListResourceBundle позволяет использовать ресурсы любого типа. Его абстрактный метод getContents
возвращает
двухмерный массив объектов. Ключи должны быть строками, об этом я уже говорил. Значения могут быть любыми. В этом заключается
преимущество данного класса, но в этом же и его недостаток. При изменении ресурса придется, возможно, перекомпилировать
класс, который реализует метод getContents
. Подробнее о нем я говорить не буду, этот класс очень хорошо
описан в API.
Класс java.util.PropertyResourceBundle
позволяет загружать ресурсы из файла свойств. В этом его преимущество –
изменение ресурсов не потребует перекомпиляции. Но в этом же и недостаток – ресурсы могут быть только строковыми.
Перейдем теперь к следующей теме –
Порядок поиска ресурсов
Для начала зададимся вопросом – а как вообще ResourceBundle
идентифицируется по языку? Очень просто. По тому
же самому принципу, что заложен в Locale
– язык, страна, вариант. Эти параметры должны фигурировать в названии
пакета ресурсов. Скажем, для белорусского варианта русского языка это будет ru_BY
, для британского английского –
en_GB
.
Итак. Пусть у нас есть базовое имя ресурса baseName
, и ресурс нам нужен для locale1
,
определяемого параметрами language1
, country1
и variant1
. Locale
по
умолчанию пусть будет определен через language2
, country2
и variant2
. Мы вызываем
ResourceBundle.getBundle(baseName, locale1)
. Что происходит?
- Сначала формируется следующий список строк (т.н. кандидаты в имена пакетов):
- baseName + "_"+ language1 + "_" + country1 + "_" + variant1
- baseName + "_"+ language1 + "_" + country1
- baseName + "_"+ language1
- baseName + "_"+ language2 + "_" + country2 + "_" + variant2
- baseName + "_"+ language2 + "_" + country2
- baseName + "_"+ language2
- baseName
getBundle
начинает перебирать строки именно в таком порядке, пытаясь загрузить ресурс. Для каждого кандидата он:
-
Пытается загрузить класс с таким именем. Если такой класс может быть загружен, если он является наследником
java.util.ResourceBundle
, если он доступен изjava.util.ResourceBundle
и если он может быть создан –getBundle
создает его и возвращает как результирующий пакет ресурсов. -
Если загрузка не удалась –
getBundle
начинает поиск файла свойств. Из имени кандидата он генерирует путь, заменяя все "." на "/", добавляя "/" впереди и.properties
в конце. Далее через методClassLoader.getResource
он пытается загрузить файл ресурса и создать на его основе экземпляр классаjava.util.PropertyResourceBundle
. Если это получилось – полученый объект возвращается как результирующий пакет ресурсов. - Если не удалось загрузить файл ресурсов – берется следующий кандидат в имя пакета. Если же их больше нет – выбрасывается
исключение
java.util.MissingResourceException
.
-
Пытается загрузить класс с таким именем. Если такой класс может быть загружен, если он является наследником
- Итак, пакет загружен. Для него создается родительская цепочка. Родительские имена определяются путем последовательного
исключения из имени варианта, страны и языка. К примеру, если у нас
baseName="main"
,language="en"
иcountry="GB"
, то имя пакета будетmain_en_GB
, а его родителей –main_en
иmain
. Если, скажем,main_en
не может быть загружен, то непосредственным родителем будетmain
. Родительские пакеты прописываются дочерним через методsetParent
и используются при поиске данных. По окончании формирования родительской цепочки процесс загрузки завершен.
После того, как пакет ресурсов загружен, поиск ресурса по ключу происходит следующим образом. Если ресурс найден в пакете –
он возвращается. Иначе вызов делегируется родителю. Если вызов дошел до последнего родителя, а ресурс не найден –
выбрасывается исключение java.util.MissingResourceException
.
Хочу обратить особое внимание на порядок поиска. Как видите, в списке фигурируют имена пакетов, соответствующих языку
по умолчанию! Что означает следующее: если у вас есть, скажем, пакеты main_ru
и main
, язык по
умолчанию русский, то при попытке загрузить пакет для английского языка будет загружен пакет для русского,
main_ru
! А вовсе не main
, как можно было бы предположить.
Иллюстративный пример. Возьмите вот это архив – resLookupOrder.zip, –
распакуйте в отдельную директорию и запустите проект с помощью ant
. Форма содержит три кнопки – для открытия
диалога с русским, французским и английским UI. С первыми двумя все нормально, а вот вместо английского загружается русский.
Происходит это потому, что в директории build/classes/resources
нет файла loc_data_en.properties
.
Соответственно, используется loc_data_ru.properties
. Если же выполнить команду ant add-en
, то
ситуация выправится, несмотря на то, что появившийся файл loc_data_en.properties
– ПУСТОЙ! Все данные будут
браться из родительского файла – loc_data.properties
. Сам же loc_data_en.properties
служит только
для того, чтобы не загружался пакет для русского языка. Да, разумеется, это будет работать только если в системе язык по
умолчанию русский.
Еще пара замечаний, насчет базовых имен. Как я уже говорил, пакет ресурсов может быть либо классом, либо файлом свойств.
Соответственно, в первом случае базовое имя должно быть полным именем класса, вместе с именем пакета, но БЕЗ языковых
суффиксов. Если, скажем, вы реализовали пакет в виде классов ru.myname.MyBundle
,
ru.myname.MyBundle_en
, ru.myname.MyBundle_en_GB
, ru.myname.MyBundle_ru
, то базовым
именем для них будет ru.myname.MyBundle
. Языковые суффиксы будут добавлены при поиске.
Если же пакет сформирован в виде файлов свойств, то вы должны обеспечить доступность этих файлов загрузчику классов.
Т.е. включить их в classpath. Именем пакета в этом случае будет относительный путь к файлу. Скажем, если в classpath включена
директория ./classes
, а внутри нее в директории resources
находятся файлы свойств
mybundle.properties
, mybundle_en.properties
и mybundle_ru.properties
, то имя пакета
будет resources.mybundle
– путь относительно директории classes
с замененными на точки
разделителями. Теоретически для обратной совместимости поддерживаются и имена БЕЗ замещения точек
(resources/mybundle
), но я бы не рекомендовал их использовать, ибо они работают только в случае файлов свойств,
класс вы так не загрузите.
И последняя тема –
Загрузка ресурсов из файлов свойств
Загрузка ресурсов из файлов свойств производится с помощью класса java.util.Properties
. По сути своей этот
класс – хеш-таблица, со строковыми ключами и строковыми же значениями. И есть у этого класса особенность – он предполагает,
что входной файл записан в кодировке iso-8859-1. Я об этом уже писал в статье о
кодировках. Для всех символов, не входящих в эту кодировку, необходимо использовать escape-последовательность вида
\uXXXX
, где XXXX – код символа в UTF-16.
К счастью, вручную этого замещения делать не придется. В состав J2SDK входит утилита native2ascii
, которая
предназначена именно для этого. Ей можно задать кодировку входного файла, чтобы иметь возможность конвертировать не только
файлы, написаные в кодировке по умолчанию операционной системы. Также эта утилита умеет производить обратную транформацию.
Подробнее о работе с ней можно почитать тут: http://java.sun.com/j2se/1.5.0/docs/tooldocs/index.html#intl.
Вернемся к примеру выше. Для французского языка, например, символ 'é' замещается на \u00E9
(теоретически замещения быть не должно, ибо этот символ входит в iso-8859-1, но native2ascii
зачем-то его делает). И строка в результирующем файле при этом выглядит так:
disabled=Handicap\u00e9e
Для русского же языка заменяются все буквы русского алфавита, в результате чего файл свойств становится слабо читаемым:
title=\u0414\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b bigAlphabet=\u0410-\u042F smallAlphabet=\u0430-\u044F enabled=\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d disabled=\u0417\u0430\u043f\u0440\u0435\u0449\u0435\u043d
В приведенном выше примере я включил в сборку исходные файлы в native-кодировках. Можете сами для
интереса прогнать их через native2ascii
.
Честно сказать, необходимость использовать native2ascii
меня довольно сильно раньше раздражала. Я даже начал
писать свой вариант загрузчика файла свойств, который умеет работать с различными кодировками. Однако в процессе я столкнулся
с таким количеством тонкостей и проблем, что пришел к выводу, что существующая система оптимальна. Трансляцию же из
native-кодировки в iso-8859-1 можно автоматизировать, если в этом возникнет необходимость.
С другой стороны, никто не мешает написать свою реализацию ResourceBundle
, которая будет работать с файлами
в native-кодировках. Это может быть как раз тем случаем, такое решение оправдано, особенно если его реализовать как общий
фреймворк. И, кстати, там есть над чем подумать.
* * *
Пожалуй, это все, что я хотел расказать о локализации. Скорее всего большая часть этого вам уже была знакома. Я лишь хотел подчеркнуть некоторые тонкости, которые знают далеко не все, и которые могут доставить немало головной боли. Например, то, что язык по умолчанию имеет приоритет над базовым пакетом. Мы в свое время немало намучались, пока нашли, откуда в UI неожиданно вылезает русский текст.
На этом прощаюсь. Всем спасибо за внимание!