feat: in-app display language switcher (Settings → Appearance) (#604)
Add a Language selector to Settings → Appearance that overrides the app display language independent of the device locale. Persists a BCP-47 code in ServerStoreState.appLanguage; MainActivity.attachBaseContext applies it via a ContextWrapper (works on minSdk 26, no AppCompat dependency needed since the Activity is a plain ComponentActivity). Selecting a language recreates the activity so resources re-resolve immediately. Includes System default / English / 한국어 options, plus a unit test for the locale-code resolver. Note: the new string keys (settings_item_language, language_system, language_english, language_korean) should be backfilled into values-ko once PR #602 lands. Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
4314bddaae
commit
b3765bd106
9 files changed
+200
No files matched your search
@@ -163,6 +163,8 @@ fun SettingsScreen(
|
||||
onUseDynamicColorsChange = viewModel::onUseDynamicColorsChange,
|
||||
themePreset = state.themePreset,
|
||||
onThemePresetChange = viewModel::onThemePresetChange,
|
||||
appLanguage = state.appLanguage,
|
||||
onAppLanguageChange = viewModel::onAppLanguageChange,
|
||||
)
|
||||
|
||||
ChatSection(
|
||||
|
||||
@@ -39,6 +39,7 @@ data class SettingsUiState(
|
||||
val selectedProfileId: String? = null,
|
||||
val renameProfileName: String = "",
|
||||
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
|
||||
val appLanguage: String = "system",
|
||||
// Profile add/edit dialog
|
||||
val showProfileDialog: Boolean = false,
|
||||
val editingProfileId: String? = null,
|
||||
@@ -79,6 +80,7 @@ class SettingsViewModel(
|
||||
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
|
||||
val profiles = AuthManager.getConnectionProfiles()
|
||||
val bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode()
|
||||
val appLanguage = AuthManager.getAppLanguage()
|
||||
val renameProfileName =
|
||||
profiles.firstOrNull { p -> p.id == selectedId }?.name ?: ""
|
||||
_uiState.update {
|
||||
@@ -97,6 +99,7 @@ class SettingsViewModel(
|
||||
selectedProfileId = selectedId,
|
||||
renameProfileName = renameProfileName,
|
||||
bottomNavDisplayMode = bottomNavDisplayMode,
|
||||
appLanguage = appLanguage,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -296,6 +299,11 @@ class SettingsViewModel(
|
||||
AuthManager.setThemePreset(preset)
|
||||
}
|
||||
|
||||
fun onAppLanguageChange(code: String) {
|
||||
_uiState.update { it.copy(appLanguage = code, isSaved = false) }
|
||||
AuthManager.setAppLanguage(code)
|
||||
}
|
||||
|
||||
fun onBottomNavDisplayModeChange(mode: BottomNavDisplayMode) {
|
||||
_uiState.update { it.copy(bottomNavDisplayMode = mode, isSaved = false) }
|
||||
AuthManager.setBottomNavDisplayMode(mode)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.m57.hermescontrol.ui.settings.components
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -43,6 +44,7 @@ import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
||||
import com.m57.hermescontrol.theme.ThemePreference
|
||||
import com.m57.hermescontrol.theme.ThemePreset
|
||||
import com.m57.hermescontrol.ui.settings.SectionCard
|
||||
import com.m57.hermescontrol.util.LocaleContextWrapper
|
||||
|
||||
@Composable
|
||||
internal fun AppearanceSection(
|
||||
@@ -52,6 +54,8 @@ internal fun AppearanceSection(
|
||||
onUseDynamicColorsChange: (Boolean) -> Unit,
|
||||
themePreset: ThemePreset,
|
||||
onThemePresetChange: (ThemePreset) -> Unit,
|
||||
appLanguage: String,
|
||||
onAppLanguageChange: (String) -> Unit,
|
||||
) {
|
||||
SectionCard(title = stringResource(R.string.settings_sec_appearance)) {
|
||||
Text(
|
||||
@@ -193,6 +197,42 @@ internal fun AppearanceSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.settings_item_language),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val languageOptions =
|
||||
listOf(
|
||||
LocaleContextWrapper.SYSTEM_LANGUAGE to stringResource(R.string.language_system),
|
||||
"en" to stringResource(R.string.language_english),
|
||||
"ko" to stringResource(R.string.language_korean),
|
||||
)
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
val activity = LocalActivity.current
|
||||
languageOptions.forEachIndexed { index, (code, label) ->
|
||||
SegmentedButton(
|
||||
selected = appLanguage == code,
|
||||
onClick = {
|
||||
onAppLanguageChange(code)
|
||||
// MainActivity is a plain ComponentActivity (not
|
||||
// AppCompatActivity), so the locale only takes effect
|
||||
// after the activity is recreated.
|
||||
activity?.recreate()
|
||||
},
|
||||
shape =
|
||||
SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = languageOptions.size,
|
||||
),
|
||||
) {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user