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)
         }
     }
 }