manager: Optimize Language Settings

- Do not update language configurations by refreshing activities; instead, introduce consistent language configurations from kernelsu-next.

Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com>
This commit is contained in:
ShirkNeko
2025-10-16 22:37:52 +08:00
parent cd78c2693a
commit af25f8d49e
45 changed files with 422 additions and 427 deletions

View File

@@ -1,95 +1,27 @@
package com.sukisu.ultra
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityOptions
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.system.Os
import coil.Coil
import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
import java.util.*
import java.util.Locale
@SuppressLint("StaticFieldLeak")
lateinit var ksuApp: KernelSUApplication
class KernelSUApplication : Application() {
private var currentActivity: Activity? = null
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
currentActivity = activity
}
override fun onActivityStarted(activity: Activity) {
currentActivity = activity
}
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
if (currentActivity == activity) {
currentActivity = null
}
}
}
override fun attachBaseContext(base: Context) {
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var context = base
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(base.resources.configuration)
config.setLocale(locale)
context = base.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
@SuppressLint("ObsoleteSdkInt")
override fun getResources(): Resources {
val resources = super.getResources()
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return createConfigurationContext(config).resources
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
return resources
}
lateinit var okhttpClient: OkHttpClient
override fun onCreate() {
super.onCreate()
ksuApp = this
// 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
Platform.setHiddenApiExemptions()
val context = this
@@ -107,45 +39,18 @@ class KernelSUApplication : Application() {
if (!webroot.exists()) {
webroot.mkdir()
}
// Provide working env for rust's temp_dir()
Os.setenv("TMPDIR", cacheDir.absolutePath, true)
okhttpClient =
OkHttpClient.Builder().cache(Cache(File(cacheDir, "okhttp"), 10 * 1024 * 1024))
.addInterceptor { block ->
block.proceed(
block.request().newBuilder()
.header("User-Agent", "SukiSU/${BuildConfig.VERSION_CODE}")
.header("Accept-Language", Locale.getDefault().toLanguageTag()).build()
)
}.build()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
applyLanguageSetting()
}
@SuppressLint("ObsoleteSdkInt")
private fun applyLanguageSetting() {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
// 添加刷新当前Activity的方法
fun refreshCurrentActivity() {
currentActivity?.let { activity ->
val intent = activity.intent
activity.finish()
val options = ActivityOptions.makeCustomAnimation(
activity, android.R.anim.fade_in, android.R.anim.fade_out
)
activity.startActivity(intent, options.toBundle())
}
}
}
}

View File

@@ -2,7 +2,6 @@ package com.sukisu.ultra.ui
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -26,6 +25,7 @@ import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.theme.KernelSUTheme
@@ -54,19 +54,14 @@ class MainActivity : ComponentActivity() {
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
private lateinit var themeChangeObserver: ThemeChangeContentObserver
// 标记避免重复初始化
private var isInitialized = false
override fun attachBaseContext(newBase: Context) {
val context = LocaleUtils.applyLocale(newBase)
super.attachBaseContext(context)
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(newBase?.let { LocaleHelper.applyLanguage(it) })
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
// 确保应用正确的语言设置
LocaleUtils.applyLanguageSetting(this)
// 应用自定义 DPI
DisplayUtils.applyCustomDpi(this)
@@ -271,7 +266,6 @@ class MainActivity : ComponentActivity() {
override fun onResume() {
try {
super.onResume()
LocaleUtils.applyLanguageSetting(this)
ThemeUtils.onActivityResume()
// 仅在需要时刷新数据
@@ -311,13 +305,4 @@ class MainActivity : ComponentActivity() {
e.printStackTrace()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
try {
super.onConfigurationChanged(newConfig)
LocaleUtils.applyLanguageSetting(this)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -1,9 +1,6 @@
package com.sukisu.ultra.ui.activity.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -236,44 +233,4 @@ object DisplayUtils {
}
}
}
}
object LocaleUtils {
@SuppressLint("ObsoleteSdkInt")
fun applyLanguageSetting(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = context.resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
fun applyLocale(context: Context): Context {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var newContext = context
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
newContext = context.createConfigurationContext(config)
}
return newContext
}
}

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.component.ImageEditorDialog
@@ -38,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import zako.zako.zako.zakoui.screen.moreSettings.component.ColorCircle
import zako.zako.zako.zakoui.screen.moreSettings.component.LanguageSelectionDialog
import zako.zako.zako.zakoui.screen.moreSettings.component.MoreSettingsDialogs
import zako.zako.zako.zakoui.screen.moreSettings.component.SettingItem
import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsCard
@@ -171,13 +173,7 @@ private fun AppearanceSettings(
) {
SettingsCard(title = stringResource(R.string.appearance_settings)) {
// 语言设置
SettingItem(
icon = Icons.Default.Language,
title = stringResource(R.string.language_setting),
subtitle = state.supportedLanguages.find { it.first == state.currentLanguage }?.second
?: stringResource(R.string.language_follow_system),
onClick = { state.showLanguageDialog = true }
)
LanguageSetting(state = state)
// 主题模式
SettingItem(
@@ -713,4 +709,40 @@ private fun DimSlider(
fun saveCardConfig(context: Context) {
CardConfig.save(context)
}
@Composable
private fun LanguageSetting(state: MoreSettingsState) {
val context = LocalContext.current
val language = stringResource(id = R.string.settings_language)
// Compute display name based on current app locale
val currentLanguageDisplay = remember(state.currentAppLocale) {
val locale = state.currentAppLocale
if (locale != null) {
locale.getDisplayName(locale)
} else {
context.getString(R.string.language_system_default)
}
}
SettingItem(
icon = Icons.Filled.Translate,
title = language,
subtitle = currentLanguageDisplay,
onClick = { state.showLanguageDialog = true }
)
// Language Selection Dialog
if (state.showLanguageDialog) {
LanguageSelectionDialog(
onLanguageSelected = { newLocale ->
// Update local state immediately
state.currentAppLocale = LocaleHelper.getCurrentAppLocale(context)
// Apply locale change immediately for Android < 13
LocaleHelper.restartActivity(context)
},
onDismiss = { state.showLanguageDialog = false }
)
}
}

View File

@@ -1,12 +1,10 @@
package zako.zako.zako.zakoui.screen.moreSettings
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
@@ -14,13 +12,11 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.util.*
import com.topjohnwu.superuser.Shell
import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState
import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon
import java.util.*
/**
* 更多设置处理器
@@ -136,40 +132,6 @@ class MoreSettingsHandlers(
}
}
/**
* 处理语言设置变更
*/
@SuppressLint("ObsoleteSdkInt")
fun handleLanguageChange(code: String) {
if (state.currentLanguage != code) {
prefs.edit {
putString("app_language", code)
commit()
}
state.currentLanguage = code
Toast.makeText(
context,
context.getString(R.string.language_changed),
Toast.LENGTH_SHORT
).show()
val locale = if (code.isEmpty()) Locale.getDefault() else Locale.forLanguageTag(code)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
context.resources.updateConfiguration(config, context.resources.displayMetrics)
}
ksuApp.refreshCurrentActivity()
}
}
/**
* 处理主题色变更
*/

View File

@@ -1,5 +1,6 @@
package zako.zako.zako.zakoui.screen.moreSettings.component
import android.content.Context
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
@@ -9,9 +10,17 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.*
import zako.zako.zako.zakoui.screen.moreSettings.MoreSettingsHandlers
@@ -35,19 +44,6 @@ fun MoreSettingsDialogs(
)
}
// 语言切换对话框
if (state.showLanguageDialog) {
KeyValueChoiceDialog(
title = stringResource(R.string.language_setting),
options = state.supportedLanguages,
selectedCode = state.currentLanguage,
onOptionSelected = { code ->
handlers.handleLanguageChange(code)
},
onDismiss = { state.showLanguageDialog = false }
)
}
// DPI 设置确认对话框
if (state.showDpiConfirmDialog) {
ConfirmDialog(
@@ -168,48 +164,134 @@ fun ConfirmDialog(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KeyValueChoiceDialog(
title: String,
options: List<Pair<String, String>>,
selectedCode: String,
onOptionSelected: (String) -> Unit,
fun LanguageSelectionDialog(
onLanguageSelected: (String) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
options.forEach { (code, name) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onOptionSelected(code)
onDismiss()
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedCode == code,
onClick = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(name)
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
// Check if should use system language settings
if (LocaleHelper.useSystemLanguageSettings) {
// Android 13+ - Jump to system settings
LocaleHelper.launchSystemLanguageSettings(context)
onDismiss()
} else {
// Android < 13 - Show app language selector
// Dynamically detect supported locales from resources
val supportedLocales = remember {
val locales = mutableListOf<java.util.Locale>()
// Add system default first
locales.add(java.util.Locale.ROOT) // This will represent "System Default"
// Dynamically detect available locales by checking resource directories
val resourceDirs = listOf(
"ar", "bg", "de", "fa", "fr", "hu", "in", "it",
"ja", "ko", "pl", "pt-rBR", "ru", "th", "tr",
"uk", "vi", "zh-rCN", "zh-rTW"
)
resourceDirs.forEach { dir ->
try {
val locale = when {
dir.contains("-r") -> {
val parts = dir.split("-r")
java.util.Locale.Builder()
.setLanguage(parts[0])
.setRegion(parts[1])
.build()
}
else -> java.util.Locale.Builder()
.setLanguage(dir)
.build()
}
// Test if this locale has translated resources
val config = android.content.res.Configuration()
config.setLocale(locale)
val localizedContext = context.createConfigurationContext(config)
// Try to get a translated string to verify the locale is supported
val testString = localizedContext.getString(R.string.settings_language)
val defaultString = context.getString(R.string.settings_language)
// If the string is different or it's English, it's supported
if (testString != defaultString || locale.language == "en") {
locales.add(locale)
}
} catch (_: Exception) {
// Skip unsupported locales
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
// Sort by display name
val sortedLocales = locales.drop(1).sortedBy { it.getDisplayName(it) }
mutableListOf<java.util.Locale>().apply {
add(locales.first()) // System default first
addAll(sortedLocales)
}
}
)
}
val allOptions = supportedLocales.map { locale ->
val tag = if (locale == java.util.Locale.ROOT) {
"system"
} else if (locale.country.isEmpty()) {
locale.language
} else {
"${locale.language}_${locale.country}"
}
val displayName = if (locale == java.util.Locale.ROOT) {
context.getString(R.string.language_system_default)
} else {
locale.getDisplayName(locale)
}
tag to displayName
}
val currentLocale = prefs.getString("app_locale", "system") ?: "system"
val options = allOptions.map { (tag, displayName) ->
ListOption(
titleText = displayName,
selected = currentLocale == tag
)
}
var selectedIndex by remember {
mutableIntStateOf(allOptions.indexOfFirst { (tag, _) -> currentLocale == tag })
}
ListDialog(
state = rememberUseCaseState(
visible = true,
onFinishedRequest = {
if (selectedIndex >= 0 && selectedIndex < allOptions.size) {
val newLocale = allOptions[selectedIndex].first
prefs.edit { putString("app_locale", newLocale) }
onLanguageSelected(newLocale)
}
onDismiss()
},
onCloseRequest = {
onDismiss()
}
),
header = Header.Default(
title = stringResource(R.string.settings_language),
),
selection = ListSelection.Single(
showRadioButtons = true,
options = options
) { index, _ ->
selectedIndex = index
}
)
}
}
@Composable
fun ThemeColorDialog(
onColorSelected: (ThemeColors) -> Unit,

View File

@@ -2,7 +2,6 @@ package zako.zako.zako.zakoui.screen.moreSettings.state
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
@@ -10,11 +9,11 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeConfig
import java.util.Locale
/**
* 更多设置状态管理
@@ -37,9 +36,12 @@ class MoreSettingsState(
// 动态颜色开关状态
var useDynamicColor by mutableStateOf(ThemeConfig.useDynamicColor)
// 语言设置
var showLanguageDialog by mutableStateOf(false)
var currentAppLocale by mutableStateOf(LocaleHelper.getCurrentAppLocale(context))
// 对话框显示状态
var showThemeModeDialog by mutableStateOf(false)
var showLanguageDialog by mutableStateOf(false)
var showThemeColorDialog by mutableStateOf(false)
var showDpiConfirmDialog by mutableStateOf(false)
var showImageEditor by mutableStateOf(false)
@@ -51,8 +53,6 @@ class MoreSettingsState(
var dynamicSignHash by mutableStateOf("")
var showDynamicSignDialog by mutableStateOf(false)
// 获取当前语言设置
var currentLanguage by mutableStateOf(prefs.getString("app_language", "") ?: "")
// 各种设置开关状态
var isSimpleMode by mutableStateOf(prefs.getBoolean("is_simple_mode", false))
@@ -101,49 +101,4 @@ class MoreSettingsState(
context.getString(R.string.dpi_size_large) to 420,
context.getString(R.string.dpi_size_extra_large) to 560
)
// 获取支持的语言列表
val supportedLanguages by lazy {
val languages = mutableListOf<Pair<String, String>>()
languages.add("" to context.getString(R.string.language_follow_system))
val locales = context.resources.configuration.locales
for (i in 0 until locales.size()) {
val locale = locales.get(i)
val code = locale.toLanguageTag()
if (!languages.any { it.first == code }) {
languages.add(code to locale.getDisplayName(locale))
}
}
val commonLocales = listOf(
Locale.forLanguageTag("en"), // 英语
Locale.forLanguageTag("zh-CN"), // 简体中文
Locale.forLanguageTag("zh-HK"), // 繁体中文(香港)
Locale.forLanguageTag("zh-TW"), // 繁体中文(台湾)
Locale.forLanguageTag("ja"), // 日语
Locale.forLanguageTag("fr"), // 法语
Locale.forLanguageTag("de"), // 德语
Locale.forLanguageTag("es"), // 西班牙语
Locale.forLanguageTag("it"), // 意大利语
Locale.forLanguageTag("ru"), // 俄语
Locale.forLanguageTag("pt"), // 葡萄牙语
Locale.forLanguageTag("ko"), // 韩语
Locale.forLanguageTag("vi") // 越南语
)
for (locale in commonLocales) {
val code = locale.toLanguageTag()
if (!languages.any { it.first == code }) {
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
try {
val testContext = context.createConfigurationContext(config)
testContext.getString(R.string.language_follow_system)
languages.add(code to locale.getDisplayName(locale))
} catch (_: Exception) {
}
}
}
languages
}
}

View File

@@ -0,0 +1,154 @@
package zako.zako.zako.zakoui.screen.moreSettings.util
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.provider.Settings
import java.util.*
object LocaleHelper {
/**
* Check if should use system language settings (Android 13+)
*/
val useSystemLanguageSettings: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
/**
* Launch system app locale settings (Android 13+)
*/
fun launchSystemLanguageSettings(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
} catch (_: Exception) {
// Fallback to app language settings if system settings not available
}
}
}
/**
* Apply saved language setting to context (for Android < 13)
*/
fun applyLanguage(context: Context): Context {
// On Android 13+, language is handled by system
if (useSystemLanguageSettings) {
return context
}
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val localeTag = prefs.getString("app_locale", "system") ?: "system"
return if (localeTag == "system") {
context
} else {
val locale = parseLocaleTag(localeTag)
setLocale(context, locale)
}
}
/**
* Set locale for context (Android < 13)
*/
@SuppressLint("ObsoleteSdkInt")
private fun setLocale(context: Context, locale: Locale): Context {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
updateResources(context, locale)
} else {
updateResourcesLegacy(context, locale)
}
}
@SuppressLint("UseRequiresApi", "ObsoleteSdkInt")
@TargetApi(Build.VERSION_CODES.N)
private fun updateResources(context: Context, locale: Locale): Context {
val configuration = Configuration()
configuration.setLocale(locale)
configuration.setLayoutDirection(locale)
return context.createConfigurationContext(configuration)
}
@Suppress("DEPRECATION")
@SuppressWarnings("deprecation")
private fun updateResourcesLegacy(context: Context, locale: Locale): Context {
Locale.setDefault(locale)
val resources = context.resources
val configuration = resources.configuration
configuration.locale = locale
configuration.setLayoutDirection(locale)
resources.updateConfiguration(configuration, resources.displayMetrics)
return context
}
/**
* Parse locale tag to Locale object
*/
private fun parseLocaleTag(tag: String): Locale {
return try {
if (tag.contains("_")) {
val parts = tag.split("_")
Locale.Builder()
.setLanguage(parts[0])
.setRegion(parts.getOrNull(1) ?: "")
.build()
} else {
Locale.Builder()
.setLanguage(tag)
.build()
}
} catch (_: Exception) {
Locale.getDefault()
}
}
/**
* Restart activity to apply language change (Android < 13)
*/
fun restartActivity(context: Context) {
if (context is Activity && !useSystemLanguageSettings) {
context.recreate()
}
}
/**
* Get current app locale
*/
@SuppressLint("ObsoleteSdkInt")
fun getCurrentAppLocale(context: Context): Locale? {
return if (useSystemLanguageSettings) {
// Android 13+ - get from system app locale settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
val localeManager = context.getSystemService(Context.LOCALE_SERVICE) as? android.app.LocaleManager
val locales = localeManager?.applicationLocales
if (locales != null && !locales.isEmpty) {
locales.get(0)
} else {
null // System default
}
} catch (_: Exception) {
null // System default
}
} else {
null // System default
}
} else {
// Android < 13 - get from SharedPreferences
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val localeTag = prefs.getString("app_locale", "system") ?: "system"
if (localeTag == "system") {
null // System default
} else {
parseLocaleTag(localeTag)
}
}
}
}