diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt index 83f75609aa95e1e0789b3c6fb62a06d425f483dd..7c2a17b4b99053f1a2b54122d19c456b16b86fd4 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE +import org.briarproject.briar.desktop.utils.InternationalizationUtils import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable @@ -59,6 +60,20 @@ fun SettingDetails(viewModel: SettingsViewModel) { modifier = Modifier.widthIn(min = 200.dp) ) } + + DetailItem { + Text(i18n("settings.display.language.title")) + + OutlinedExposedDropDownMenu( + values = viewModel.languageList.map { + if (it == Settings.Language.DEFAULT) i18n("settings.display.language.auto") + else it.locale.getDisplayLanguage(InternationalizationUtils.locale) + }, + selectedIndex = viewModel.selectedLanguage.value.ordinal, + onChange = { viewModel.selectLanguage(viewModel.languageList[it]) }, + modifier = Modifier.widthIn(min = 200.dp) + ) + } } } SettingCategory.CONNECTIONS -> { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt index 59fa618dda1bd33037100d2ce8c8d9103e954307..6efb19aa18296847b5ceb23bbcd864cb533c4180 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt @@ -19,12 +19,27 @@ package org.briarproject.briar.desktop.settings import org.briarproject.briar.desktop.viewmodel.SingleStateEvent +import java.util.Locale interface Settings { enum class Theme { AUTO, LIGHT, DARK } + enum class Language { + // special handling + DEFAULT, EN, + + // languages as present in resources + AR, BG, DE, ES, FA, GL, HU, IS, IT, LT, PL, RO, RU, SK, SQ, SV, TR, ZH_CN; + + val locale: Locale + get() = if (this == DEFAULT) + Locale.getDefault() + else Locale.forLanguageTag(this.name.replace('_', '-')) + } + var theme: Theme + var language: Language val invalidateScreen: SingleStateEvent<Unit> } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt index d70e0f4df32d61d44696094859d9d5dcb4d085cd..6fea77da7fd67173c6383644153685f4643ef7fc 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt @@ -18,13 +18,17 @@ package org.briarproject.briar.desktop.settings +import org.briarproject.briar.desktop.settings.Settings.Language +import org.briarproject.briar.desktop.settings.Settings.Language.DEFAULT import org.briarproject.briar.desktop.settings.Settings.Theme import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO +import org.briarproject.briar.desktop.utils.InternationalizationUtils import org.briarproject.briar.desktop.viewmodel.SingleStateEvent import java.util.prefs.Preferences import javax.inject.Inject const val PREF_THEME = "theme" +const val PREF_LANG = "language" class SettingsImpl @Inject internal constructor() : Settings { @@ -40,4 +44,21 @@ class SettingsImpl @Inject internal constructor() : Settings { prefs.flush() // write preferences to disk invalidateScreen.emit(Unit) } + + override var language: Language + get() = Language.valueOf(prefs.get(PREF_LANG, DEFAULT.name)) + set(value) { + prefs.put(PREF_LANG, value.name) + prefs.flush() // write preferences to disk + updateLocale(value) + invalidateScreen.emit(Unit) + } + + init { + updateLocale(language) + } + + private fun updateLocale(language: Language) { + InternationalizationUtils.locale = language.locale + } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt index 529f95039be0f3a7c9fcbd35f96e320bc4de0869..3a531313dd2e02465a20a27103549dfd383420fd 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt @@ -41,10 +41,14 @@ constructor( val selectedSetting = _selectedSetting.asState() val themesList = Settings.Theme.values() + val languageList = Settings.Language.values() private val _selectedTheme = mutableStateOf(settings.theme) val selectedTheme = _selectedTheme.asState() + private val _selectedLanguage = mutableStateOf(settings.language) + val selectedLanguage = _selectedLanguage.asState() + fun selectSetting(selectedOption: SettingCategory) { _selectedSetting.value = selectedOption } @@ -53,4 +57,9 @@ constructor( settings.theme = theme _selectedTheme.value = theme } + + fun selectLanguage(language: Settings.Language) { + settings.language = language + _selectedLanguage.value = language + } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 659f8ed26344d2ae0a6c98644b79300517176ade..68640050ce0804795d06f4b123819e9883d07c6a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -115,9 +115,6 @@ constructor( @OptIn(ExperimentalComposeUiApi::class) @Composable override fun start(onClose: () -> Unit) { - // invalidate whole application window in case the theme or language setting is changed - settings.invalidateScreen.react {} - val title = i18n("main.title") val platformLocalization = object : PlatformLocalization { override val copy = i18n("copy") @@ -139,6 +136,12 @@ constructor( LocalDesktopFeatureFlags provides desktopFeatureFlags, LocalLocalization provides platformLocalization, ) { + // invalidate whole application window in case the theme or language setting is changed + settings.invalidateScreen.react { + window.title = i18n("main.title") + return@CompositionLocalProvider + } + var showAbout by remember { mutableStateOf(false) } val settingsViewModel: SettingsViewModel = viewModel() val isDarkTheme = settings.theme == DARK || diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/InternationalizationUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/InternationalizationUtils.kt index 0ac3804fb73d673fd6df5b910dcb7c73c57bbc5e..b26d4721265326639357e3267e331a0e3bffbf01 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/utils/InternationalizationUtils.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/InternationalizationUtils.kt @@ -35,6 +35,12 @@ object InternationalizationUtils { private val LOG = KotlinLogging.logger {} + /** + * The current [Locale] to be used for translations. + * Will be set during startup according to the user settings. + */ + var locale: Locale = Locale.getDefault() + /** * Returns the translated text of the string identified with `key` */ @@ -57,10 +63,10 @@ object InternationalizationUtils { fun i18nP(key: String, amount: Int): String = try { val pattern: String = i18n(key) - val messageFormat = MessageFormat(pattern, Locale.getDefault()) + val messageFormat = MessageFormat(pattern, locale) messageFormat.format(arrayOf(amount)) } catch (e: IllegalArgumentException) { - LOG.w { "Pattern does not match arguments for resource '$key' and locale '${Locale.getDefault()}" } + LOG.w { "Pattern does not match arguments for resource '$key' and locale '$locale" } "" } @@ -79,8 +85,10 @@ object InternationalizationUtils { /** * Returns the resource bundle used for i18n at Briar Desktop */ - private fun createResourceBundle(): ResourceBundle { - val locale = Locale.getDefault() - return ResourceBundle.getBundle("strings.BriarDesktop", locale) - } + private fun createResourceBundle() = + ResourceBundle.getBundle( + "strings.BriarDesktop", + if (locale.toLanguageTag() == "en") Locale.ROOT // NON-NLS + else locale + ) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt index 3a1cd50620aed8bc904a0703bbc9315f2061ba25..bdeb5d8980746620f5d355e8e63dae887eff3a62 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt @@ -35,7 +35,10 @@ import androidx.compose.runtime.mutableStateOf * only use this class if you are sure that you actually need it. */ class SingleStateEvent<T : Any> { - private var state = mutableStateOf<T?>(null) + /** + * Internal representation of state. Please don't use this directly! + */ + var state = mutableStateOf<T?>(null) /** * Emit a new value of type [T] for this event. @@ -44,22 +47,19 @@ class SingleStateEvent<T : Any> { state.value = value } - fun reactAndReset(block: (T) -> Unit) { - val value = state.value - if (value != null) { - state.value = null - block(value) - } - } - /** * React to every new value of type [T] emitted through this event, * by directly reading the state in the calling function. * This can be used to invalidate a composable function. * Make sure to not react to the same event on multiple places. */ - @Composable - inline fun react(noinline block: (T) -> Unit) = reactAndReset(block) + inline fun react(block: (T) -> Unit) { + val value = state.value + if (value != null) { + state.value = null + block.invoke(value) + } + } /** * React to every new value of type [T] emitted through this event @@ -69,7 +69,7 @@ class SingleStateEvent<T : Any> { @Composable fun reactInCoroutine(block: (T) -> Unit) { LaunchedEffect(state.value) { - reactAndReset(block) + react(block) } } }