Локализация Android-приложений — намного более сложная задача, чем должна была бы быть. Описание в документации недостаточное: чтобы разобраться в происходящем «под капотом», нужно искать информацию во внешних источниках (на StackOverflow и в блогах) и тренироваться на базовых приложениях типа «Hello World».
В этой статье я разберу некоторые трудности процесса локализации, с которыми я столкнулся в своих приложениях. Решения, которые я буду приводить, не описаны в документации, поэтому я постараюсь быть максимально точным. (Примечание: термины «локаль» и «язык» в статье используются как синонимы.)
Многобукаф? Нажмите Ctrl-F и введите «вывод №».
Введение
В статье говорится о некоторых трудностях локализации, которые не рассматриваются в официальных ресурсах, — то есть, это не руководство по локализации «для чайников».
Перед чтением полезно будет изучить основы локализации Android-приложений (если вы еще не знакомы с этой темой) — прочтите следующие документы:
https://developer.android.com/guide/topics/resources/localization,
https://developer.android.com/guide/topics/resources/multilingual-support.
Проблема № 1. Приложение отображается не на том языке
Разберемся, что происходит в этом случае «под капотом». Допустим, языковые настройки телефона следующие:
Если перевести этот список в псевдокод, получим:
function language_to_use_for_my_app() {
if (my_app supports Spanish)
return Spanish;
if (my_app supports French
return French;
if my_app supports English
return English;
return default_language_of_my_app;
}
(Примечание. Язык по умолчанию для приложения определяется самим приложением. В одном случае это может быть польский, в другом — немецкий, и так далее, — это язык, файл которого размещен по пути values/strings.xml
. К сожалению, указать системе Android язык по умолчанию для конкретного приложения нельзя — и мы с этой особенностью еще столкнемся позже.)
В основе этого списка лежит вроде бы простая логика. Однако в некоторых случаях приложение использует не тот язык, который должно было бы, и переключается на свою локаль по умолчанию, когда вы этого не ожидаете.
Чтобы увидеть, как это происходит, установите языковые настройки так, как показано на скриншоте выше, а затем создайте в Android Studio новое приложение из шаблона «Empty Activity». Откройте res/values/strings.xml
и для строки app_name
(название приложения) задайте значение «App In Default Language» («Приложение на языке по умолчанию»).
<resources>
<string name="app_name">App In Default Language</string>
</resources>
Запустите приложение и проверьте, чтобы оно работало. Название приложения появится слева вверху экрана.
Теперь добавим еще два файла values.xml
:
values-fr/strings.xml
:
<resources>
<string name="app_name">App In French</string>
</resources>
values-en/strings.xml
:
<resources>
<string name="app_name">App In English</string>
</resources>
Приложение можно снова запускать, но перед этим подумайте: какое значение app_name
будет отображаться?
Разумно предположить, что это будет название на французском: испанский — предпочитаемый язык пользователя, но для этой локали файла strings.xml нет, поэтому система выберет следующий предпочитаемый язык — то есть, французский, для которого файл strings.xml
у нас есть. Верно же?
А теперь запустите приложение:
Какого… Как так? Почему не на французском? У нас проблемы с логикой?
Нет. С нашей логикой всё в порядке. Дело в другом.
При создании проекта в Android Studio с ним идут кое-какие библиотеки по умолчанию. Взгляните на файл app/build.gradle
:
В этих библиотеках содержатся файлы ресурсов для многих локалей — в том числе в них могут быть файлы values-es/strings.xml
. Поэтому в скомпилированном и упакованном приложении у вас наверняка найдутся файлы strings.xml
на испанском.
В итоге Android будет считать, что в приложении есть испанская локаль, и попытается найти app_name
на испанском. Но мы эту переменную не определили, поэтому приложение берет ее в strings.xml
по умолчанию и, соответственно, отображает строку «App In Default Language».
Такая ситуация называется загрязнением ресурсов: Android наступает на свои же грабли. Подробнее — здесь и здесь.
Чтобы обойти эту проблему, нужно объявить поддерживаемые языки в файле app/build.gradle
.
android {
defaultConfig {
resConfigs "fr", "en" // порядок элементов списка не имеет значения
}
}
Таким образом мы удаляет все ресурсы, за исключением французской и английской локали: это позволяет не только избежать загрязнения ресурсов, но и снизить размер APK-файла.
Измените файл app/build.gradle
так, как показано выше, и перезапустите приложение:
Отлично. А если удалить французский язык из списка поддерживаемых?
Работает как ожидалось… за исключением одного случая.
«Один случай»
Давайте кое-что немного поменяем. Приведем список предпочитаемых языков к такому виду:
Теперь удалим папку resources/values-en
из проекта и добавим новый файл values-es/strings.xml
:
<resources>
<string name="app_name">App in Spanish</string>
</resources>
Итак, теперь у нас есть файл по умолчанию strings.xml
(по-прежнему со строкой «App In Default Language»), его французская и испанская версии.
Удалим английский из списка поддерживаемых языков и добавим испанский:
android {
defaultConfig {
resConfigs "fr", "es"
}
}
Прежде чем запускать приложение, подумайте: на каком языке оно будет отображаться?
Если следовать той же логике, что и раньше, это должен быть испанский.
А теперь запустите приложение:
Черт побери… опять?! Мы же только что всё исправили! Или нет?
Поэкспериментировав несколько часов с таким поведением, я пришел к следующему заключению: где-то в кодовой базе Android предполагается, что ресурсы по умолчанию — на английском языке.
Это, на мой взгляд, неверно, но что уж поделаешь.
Выше я говорил, что resConfig
удаляет ресурсы всех локалей, кроме объявленных… но я соврал: ресурсы по умолчанию resConfig
НЕ УДАЛЯЕТ.
Итак, что у нас есть? В настройках пользователя английский указан как предпочитаемый язык. В пакете нашего приложения есть кое-какие строковые файлы по умолчанию (ведь они не были удалены), Android считает, что они на английском, и поэтому решает использовать файлы ресурсов по умолчанию.
Вывод № 1. Обязательно объявляйте языки, поддерживаемые в приложении, — с помощью
resConfigs
.Вывод № 2. Если нужно, чтобы приложение поддерживало английский язык, файлы ресурсов по умолчанию должны быть на английском: Android рассчитывает на такое положение дел, поэтому так у вас будет меньше головной боли.
(Если вам английский в приложении не нужен, пропустите второй вывод.)
Проблема № 2. Как узнать текущую локаль приложения?
Казалось бы, найти ответ на такой вопрос должно быть легко, но, к сожалению, это не так: погуглив, вы увидите, что на StackOverflow об этом спрашивали, и не раз.
Причем там есть несколько вводящих в заблуждение ответов, например:
Locale.getDefault()
getResources().getConfiguration().getLocales().get(0);
getResources().getConfiguration().locale
О каждом из этих методов я расскажу подробнее. Но прежде проясним кое-что очевидное, что путают советчики на StackOverflow:
Язык устройства != язык приложения
Первое — это язык, выбранный пользователем в настройках устройства. Второе — это язык, который Android решил использовать для конкретного приложения.
Узнать значение для языка устройства легко:
Configuration config = Resources.getSystem().getConfiguration();
String locale = config.getLocales().get(0);
// или, если у вас уровень API < 24
String locale = config.locale;
Итак, с языком устройства мы разобрались — перейдем к языку приложения. К сожалению, официального способа узнать локаль приложения во время выполнения, гарантированно не наткнувшись на ошибку, не существует.
Еще раз: не существует официального способа надежным образом узнать язык приложения во время выполнения.
Почему? Рассмотрим два случая:
Случай 1. Android нашел соответствующий файл «strings.xml» и использует его.
Случай 2. Android не нашел соответствующий файл «strings.xml» и использует файл по умолчанию.
В первом случае следующий код всегда будет возвращать правильное значение:
getResources().getConfiguration().getLocales().get(0);
// или getResources().getConfiguration().locale, если у вас
// уровень API менее 24
[[[[ НАЧАЛО БОЛЬШОГО ПРИМЕЧАНИЯ. Ребята на StackOverflow предлагают использовать Locale.getDefault()
. Чем это отличается от только что описанного способа?
Я рекомендую почитать документацию по классу Locale. Если вкратце, то Locale.getDefault()
используется для целей самой JVM: некоторые операции зависят от локали (например, выбор формата дат), и если не указать для них язык, JVM будет использовать значение, получаемое методом Locale.getDefault()
.
Но самое главное отличие в том, что Locale.getDefault()
определяется во время запуска приложения, и единственный способ изменить это значение — вызвать Locale.setDefault(новаяЛокаль) самостоятельно. Другими словами, если пользователь изменит настройки языка по умолчанию во время работы приложения, то Locale.getDefault()
всё равно будет возвращать значение, определенное во время запуска.
А вот метод getResources().getConfiguration().getLocales().get(0)
всегда будет давать актуальное значение (если вы его не кешировали, конечно).
КОНЕЦ БОЛЬШОГО ПРИМЕЧАНИЯ ]]]]
Ладно. Это был первый случай. А что насчет второго?
К сожалению, для случая 2 получить текущую локаль приложения нельзя. Нет официального способа узнать, какой файл strings.xml
используется системой Android: локализованный или по умолчанию. Если применить способ для случая 1, мы просто получим локаль устройства.
Еще раз: если у вас случай 2, то метод getResources().getConfiguration().getLocales.get(0)
вернет вам локаль устройства, а не приложения.
Однако для решения этой задачи есть обходной путь: нужно добавить в каждый из файлов strings.xml специальную строку (допустим, это будет current_locale
). В испанской версии strings.xml
будет current_locale = 'es'
, в итальянской — current_locale = 'it'
, в файле по умолчанию — current_locale = 'en'
(смотрите второй вывод статьи). Теперь достаточно будет в коде приложения вызвать следующий метод: getString(R.strings.current_locale)
Вывод № 3. Получение текущей локали устройства:
Resources.getSystem().getConfiguration().getLocales().get(0)
. Если уровень API равен 23 или ниже:Resources.getSystem().getConfiguration().locale
.Вывод № 4. Не существует надежного официального способа получить текущую локаль стандартными методами. Но есть обходной путь — см. чуть выше.
Проблема № 3. Как получить список предпочитаемых языков устройства?
В руководствах по Android сказано, что для этого есть новый API — LocaleList
(для Android API с уровня 24). Теоретически, при вызове LocaleList.getDefault()
вы должны получить список предпочитаемых языков пользователя, заданный в настройках, и он не должен зависеть от приложения — по крайней мере, так говорится в руководствах…
Я поэкспериментировал с LocaleList.getDefault()
и могу сказать, что этот метод не всегда возвращает список предпочитаемых языков точно как в настройках.
В каких случаях бывает несоответствие? Проиллюстрирую на примере: предположим, немецкий НЕ ВЫБРАН как предпочитаемый язык и ваше приложение его тоже не поддерживает. Сделаем так:
Locale.setDefault(new Locale('de'));
LocaleList.getDefautlt(); // в списке будет немецкий
Каким-то образом в списке, возвращенном методом LocaleList.getDefault()
, оказался немецкий… хотя ни телефон, ни приложение его не поддерживают. Смотрим документацию LocaleList.getDefault()
:
"Результат обязательно включает в себя локаль по умолчанию, получаемую из Locale.getDefault(), но не обязательно в верхней части списка. Если локаль по умолчанию не вверху списка, это значит, что система установила в качестве ее одну из других предпочитаемых локалей пользователя, заключив, что основной вариант не поддерживается, но вторичный поддерживается.
Внимание: для API >= 24 список LocaleList по умолчанию изменится, если вызвать Locale.setDefault(). В этом методе это учитывается: проверяется вывод Locale.getDefault() и при необходимости пересчитывается список LocaleList по умолчанию."
Чего-о-о? Я прочел это трижды и всё равно не понял.
Давайте лучше я сам расскажу, как получить список предпочитаемых локалей на устройстве — и это будет последний вывод статьи.
Вывод № 5. Получение списка предпочитаемых локалей устройства (заданных в настройках):
Resources.getSystem().getConfiguration().getLocales()
. Это применимо только в API уровня 24 и выше: раньше в качестве предпочитаемого пользователь мог выбрать только один язык.
Заключение
В документации (а также в кодовой базе) Android не учитываются многие варианты использования локализации, и это может быть неприятно, поскольку разработчикам приходится учитывать крайние случаи, и делается это не всегда удобным и логичным образом.
Надеюсь, в этой статье мне удалось объяснить, как Android работает с локализацией, и какой API лучше выбрать для конкретного случая.
О переводчике
Перевод статьи выполнен в Alconost.
Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.
Мы также делаем рекламные и обучающие видеоролики — для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.