Локализация 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.

Комментарии (2)


  1. Dragonfly86
    07.10.2021 14:59
    +1

    Спасибо, интересненько) я правда скорее чайник в этой теме, но все так хорошо разжевано, что в целом понятно и новичку. Еще понравился неформальный стиль автора ????


  1. Slim0788
    18.10.2021 11:51
    +1

    Есть способ вылечить случай «Один случай». В строковых ресурсах по умолчанию можно сослаться на самого себя. Тогда всё должно прокатить. В разрезе статьи это выглядело бы вот так:

    <resources>
      <string name="app_name">app_name</string>
    </resources>