В сегодняшнем глобализованном мире пользователи ожидают, что приложения будут доступны на нескольких языках в соответствии с их уникальными языковыми предпочтениями. Например, пользователь может предпочесть читать новости на испанском языке, просматривать веб-сайты на английском языке и общаться с семьей на китайском языке. Благодаря функции Языковые настройки для каждого приложения, представленной в Android 13, разработчики могут предоставить пользователям многоязычный интерфейс, позволяя им беспрепятственно переключаться между разными языками в приложении. Это улучшает взаимодействие с пользователем, делая приложение более доступным и привлекательным, а также помогает разработчикам обслуживать более широкую аудиторию.

В этой статье я проведу вас через процесс реализации Языковых настроек для каждого приложения в вашем приложении для Android, чтобы вы могли воспользоваться этой функцией и предоставить своим пользователям по-настоящему персонализированный опыт. Давайте погрузимся!

Настраивать

Допустим, у нас есть простое приложение для выбора фотографий, которое позволяет вам выбирать изображения и отображать их в приложении. Чтобы начать работу, вы можете загрузить исходный код установки ЗДЕСЬ.

Вы можете заметить, что файлы string.xml, поддерживаемые нашим приложением языки, уже присутствуют в коде для английского (по умолчанию), арабского, французского и голландского языков.

Запустите приведенный выше код и на своем устройстве (Android 13 или более поздней версии); если вы перейдете к Settings -> System -> Language & Input -> App Languages, вы заметите, что мы не видим наше приложение в списке поддерживаемых для «Языков приложений».

Добавление поддерживаемой языковой конфигурации

Для начала давайте создадим новый XML-файл locales_config.xml(app/res/XML) и добавим сюда языки, поддерживаемые нашим приложением.

<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="en" />
    <locale android:name="de" />
    <locale android:name="fr" />
    <locale android:name="ar" />
</locale-config>

Также добавьте этот файл конфигурации в наш AndroidManifest.xml.

android:localeConfig="@xml/locales_config"

Теперь наше приложение начнет появляться на экране «Языки приложений» в соответствии с нашим поддерживаемым языком:

Мы можем переключить язык с системного по умолчанию на любой из поддерживаемых нами языков, и изменение языка будет отражаться только в нашем приложении, а не во всей системе.

Переключение языка приложения во время выполнения

Теперь давайте добавим раскрывающийся список языков, чтобы мы могли протестировать эту функцию в режиме реального времени.
1. Создайте класс данных Language.kt следующим образом:

data class Language(
    val name: String,
    val code: String
)

2. В MainActivity.kt добавьте следующие переменные:

var mExpanded by remember { mutableStateOf(false) }
var mSelectedText by remember { mutableStateOf("") }
val languages = listOf(
    Language("English", "en"),
    Language("Deutsch", "de"),
    Language("Français", "fr"),
    Language("عربي", "ar")
)

Этот код инициализирует две изменяемые переменные состояния mExpanded и mSelectedText и создает список поддерживаемых языков.

val selectedLocale = AppCompatDelegate.getApplicationLocales()[0]?.language
?: "en"
mSelectedText = languages.firstOrNull {it.code == selectedLocale}?.name ?:
"English"

Этот код извлекает предпочтительный язык приложения и устанавливает для переменной mSelectedText имя соответствующего языка из списка languages. Он использует функцию getApplicationLocales для получения предпочтительного языкового стандарта приложения и функцию firstOrNull для поиска первого подходящего объекта Language в списке languages. Если соответствующий объект не найден, языком по умолчанию является английский.

3. Давайте добавим наш выпадающий код пользовательского интерфейса под описанием Текст, который можно компоновать в MainActivity.kt

Divider(
    modifier = Modifier.padding(16.dp).fillMaxWidth(),
    color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.25f)
)

Box(modifier = Modifier.fillMaxWidth().padding(8.dp).wrapContentSize(Alignment.Center)) {
    Text(
        text = mSelectedText,
        modifier = Modifier.fillMaxWidth().padding(8.dp).clickable(onClick = { mExpanded = true })
    )

    DropdownMenu(expanded = mExpanded, onDismissRequest = { mExpanded = false }) {
        languages.forEach { language ->
            DropdownMenuItem(
                text = { Text(language.name) },
                onClick = {
                    mExpanded = false
                    val appLocale: LocaleListCompat =
                        LocaleListCompat.forLanguageTags(language.code)
                    AppCompatDelegate.setApplicationLocales(appLocale)
                    mSelectedText = AppCompatDelegate.getApplicationLocales()[0]?.language ?: "en"
                }
            )
        }
    }
}

MainActivity.kt должен выглядеть следующим образом:

package com.kamran.android13photopicker


import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import coil.compose.AsyncImage
import com.kamran.android13photopicker.ui.Language
import com.kamran.android13photopicker.ui.theme.Android13PhotoPickerTheme

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Android13PhotoPickerTheme {
                var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
                var selectedImageUris by remember { mutableStateOf<List<Uri>>(emptyList()) }

                var mExpanded by remember { mutableStateOf(false) }
                var mSelectedText by remember { mutableStateOf("") }
                val languages = listOf(
                    Language("English", "en"),
                    Language("Deutsch", "de"),
                    Language("Français", "fr"),
                    Language("عربي", "ar")
                )
                val selectedLocale = AppCompatDelegate.getApplicationLocales()[0]?.language
                    ?: "en"
                mSelectedText = languages.firstOrNull {it.code == selectedLocale}?.name ?:
                        "English"

                val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
                    contract = ActivityResultContracts.PickVisualMedia(),
                    onResult = { uri ->
                        selectedImageUri = uri
                    }
                )

                val multiplePhotoPickerLauncher = rememberLauncherForActivityResult(
                    contract = ActivityResultContracts.PickMultipleVisualMedia(),
                    onResult = { uris ->
                        selectedImageUris = uris
                    }
                )

                LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement
                    .spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
                    item {
                        Text(
                            text = stringResource(R.string.heading),
                            textAlign = TextAlign.Center,
                            style = MaterialTheme.typography.headlineLarge,
                            modifier = Modifier.fillMaxWidth().padding(16.dp)
                        )

                        Text(
                            text = stringResource(R.string.description),
                            textAlign = TextAlign.Center,
                            style = MaterialTheme.typography.bodyLarge,
                            modifier = Modifier.fillMaxWidth().padding(8.dp)
                        )

                        Divider(
                            modifier = Modifier.padding(16.dp).fillMaxWidth(),
                            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.25f)
                        )

                        Box(modifier = Modifier.fillMaxWidth().padding(8.dp).wrapContentSize(Alignment.Center)) {
                            Text(
                                text = mSelectedText,
                                modifier = Modifier.fillMaxWidth().padding(8.dp).clickable(onClick = { mExpanded = true })
                            )

                            DropdownMenu(expanded = mExpanded, onDismissRequest = { mExpanded = false }) {
                                languages.forEach { language ->
                                    DropdownMenuItem(
                                        text = { Text(language.name) },
                                        onClick = {
                                            mExpanded = false
                                            val appLocale: LocaleListCompat =
                                                LocaleListCompat.forLanguageTags(language.code)
                                            AppCompatDelegate.setApplicationLocales(appLocale)
                                            mSelectedText = AppCompatDelegate.getApplicationLocales()[0]?.language ?: "en"
                                        }
                                    )
                                }
                            }
                        }


                        Divider(
                            modifier = Modifier.padding(16.dp).fillMaxWidth(),
                            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.25f)
                        )

                        Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.SpaceAround
                        ) {
                            Button(onClick = {
                                singlePhotoPickerLauncher.launch(
                                   PickVisualMediaRequest(
                                       ActivityResultContracts.PickVisualMedia.ImageOnly
                                   )
                                )
                            }) {
                                Text(text = stringResource(R.string.single_photo_button))
                            }
                            Button(onClick = {
                                multiplePhotoPickerLauncher.launch(
                                    PickVisualMediaRequest(
                                        ActivityResultContracts.PickVisualMedia.ImageOnly
                                    )
                                )
                            }) {
                                Text(text = stringResource(R.string.multiple_photo_button))
                            }
                        }

                        Divider(
                            modifier = Modifier.padding(16.dp).fillMaxWidth(),
                            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.25f)
                        )
                    }

                    item {
                        AsyncImage(model = selectedImageUri,
                            contentDescription = null,
                            modifier = Modifier.fillMaxWidth(),
                            contentScale = ContentScale.Crop
                        )
                    }

                    items(selectedImageUris) { uri ->
                        AsyncImage(model = uri,
                            contentDescription = null,
                            modifier = Modifier.fillMaxWidth(),
                            contentScale = ContentScale.Crop
                        )
                    }
                }
            }
        }
    }
}

Смена языка происходит здесь:

val appLocale: LocaleListCompat = 
LocaleListCompat.forLanguageTags(language.code)
AppCompatDelegate.setApplicationLocales(appLocale)
mSelectedText = AppCompatDelegate.getApplicationLocales()[0]?.language ?: "en"

Этот код устанавливает предпочтительный язык приложения на язык, представленный переданным объектом Language, и обновляет переменную mSelectedText с соответствующим именем языка. Для этого создается объект LocaleListCompat с кодом языка, устанавливаются локали приложения с помощью функции setApplicationLocales и извлекается код языка с помощью функции getApplicationLocales.

🎉🎉🎉
Теперь вы можете переключаться между поддерживаемыми языками прямо из приложения
🎉🎉🎉

Обеспечение обратной совместимости

На устройствах под управлением Android 13 или более поздней версии функция языка для каждого приложения позволяет пользователям изменять языковые настройки для отдельных приложений, не затрагивая общесистемный язык. Однако при запуске того же кода на Android 12 или более ранней версии приложение вернется к системному языку по умолчанию после перезапуска. Чтобы преодолеть это ограничение, разработчикам необходимо обеспечить обратную совместимость.

Функция языка для каждого приложения предлагает преимущество обратной совместимости. Чтобы включить поддержку устройств, работающих на Android 12 (уровень API 32) и более ранних версиях, попросите AndroidX управлять хранилищем региональных настроек. Для этого установите значение autoStoreLocales на true, а android:enabled на false в записи манифеста для службы AppLocalesMetadataHolderService вашего приложения. Ниже приведен пример фрагмента кода, демонстрирующего эти настройки:

AndroidManifest.xml

<application
  ...
  <service
    android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
    android:enabled="false"
    android:exported="false">
    <meta-data
      android:name="autoStoreLocales"
      android:value="true" />
  </service>
  ...
</application>

Вот и все, ребята. Полный код можно скачать ЗДЕСЬ. Для получения дополнительной информации обратитесь к официальной документации: https://developer.android.com/guide/topics/resources/app-languages

Следите за моим техническим путешествием, следуя за мной на Medium. А если вы хотите объединить усилия и вместе покорить мир технологий, давайте общаться в LinkedIn!