Последнее изменение: 1 сентября 2007г.

Вавилонское столпотворение. Часть 2. Локализация и ресурсы

Это вторая статья из цикла "Вавилонское столпотворение", посвященного локализации приложений. В ней пойдет речь о, собственно, процессе локализации, использовании ресурсов и тонкостях, существующих в этой области.

Мы поговорим о следующих темах:

Итак. Начнем с основ –

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). Что происходит?

  • Сначала формируется следующий список строк (т.н. кандидаты в имена пакетов):

    1. baseName + "_"+ language1 + "_" + country1 + "_" + variant1
    2. baseName + "_"+ language1 + "_" + country1
    3. baseName + "_"+ language1
    4. baseName + "_"+ language2 + "_" + country2 + "_" + variant2
    5. baseName + "_"+ language2 + "_" + country2
    6. baseName + "_"+ language2
    7. 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 неожиданно вылезает русский текст.

На этом прощаюсь. Всем спасибо за внимание!