diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 8fcc6a055c..0ba8519f92 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -16,11 +16,15 @@ import java.io.FileOutputStream import java.security.KeyStore import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager +import android.content.res.Configuration +import android.os.LocaleList +import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.PowerStateUpdater +import java.util.Locale fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir @@ -73,5 +77,38 @@ class YuzuApplication : Application() { val appContext: Context get() = application.applicationContext + + private val LANGUAGE_CODES = arrayOf( + "system", "en", "es", "fr", "de", "it", "pt", "pt-BR", "ru", "ja", "ko", + "zh-CN", "zh-TW", "pl", "cs", "nb", "hu", "uk", "vi", "id", "ar", "ckb", "fa", "he", "sr" + ) + + fun applyLanguage(context: Context): Context { + val languageIndex = IntSetting.APP_LANGUAGE.getInt() + val langCode = if (languageIndex in LANGUAGE_CODES.indices) { + LANGUAGE_CODES[languageIndex] + } else { + "system" + } + + if (langCode == "system") { + return context + } + + val locale = when { + langCode.contains("-") -> { + val parts = langCode.split("-") + Locale.Builder().setLanguage(parts[0]).setRegion(parts[1]).build() + } + else -> Locale.Builder().setLanguage(langCode).build() + } + + Locale.setDefault(locale) + + val config = Configuration(context.resources.configuration) + config.setLocales(LocaleList(locale)) + + return context.createConfigurationContext(config) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 8079e9b782..58598ccdc4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -86,6 +86,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private var foregroundService: Intent? = null + override fun attachBaseContext(base: Context) { + super.attachBaseContext(YuzuApplication.applyLanguage(base)) + } + override fun onCreate(savedInstanceState: Bundle?) { Log.gameLaunched = true ThemeHelper.setTheme(this) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index d5556a337b..3bccc97607 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -32,6 +32,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { MAX_ANISOTROPY("max_anisotropy"), THEME("theme"), THEME_MODE("theme_mode"), + APP_LANGUAGE("app_language"), OVERLAY_SCALE("control_scale"), OVERLAY_OPACITY("control_opacity"), LOCK_DRAWER("lock_drawer"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index bbcfe3da45..716d3aae56 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -757,6 +757,15 @@ abstract class SettingsItem( titleId = R.string.enable_update_checks, ) ) + put( + SingleChoiceSetting( + IntSetting.APP_LANGUAGE, + titleId = R.string.app_language, + descriptionId = R.string.app_language_description, + choicesId = R.array.appLanguageNames, + valuesId = R.array.appLanguageValues + ) + ) put( SwitchSetting( BooleanSetting.RENDERER_DEBUG, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 455b3b5ff1..dd932fcafb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -1,8 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.yuzu.yuzu_emu.features.settings.ui +import android.content.Context +import org.yuzu.yuzu_emu.YuzuApplication import android.os.Bundle import android.view.View import android.view.ViewGroup.MarginLayoutParams @@ -24,6 +29,7 @@ import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment import org.yuzu.yuzu_emu.utils.* +import org.yuzu.yuzu_emu.utils.collect class SettingsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsBinding @@ -32,6 +38,10 @@ class SettingsActivity : AppCompatActivity() { private val settingsViewModel: SettingsViewModel by viewModels() + override fun attachBaseContext(base: Context) { + super.attachBaseContext(YuzuApplication.applyLanguage(base)) + } + override fun onCreate(savedInstanceState: Bundle?) { ThemeHelper.setTheme(this) @@ -125,6 +135,16 @@ class SettingsActivity : AppCompatActivity() { NativeConfig.savePerGameConfig() NativeConfig.unloadPerGameConfig() } + + if (settingsViewModel.shouldRecreateForLanguageChange.value) { + settingsViewModel.setShouldRecreateForLanguageChange(false) + val relaunchIntent = packageManager?.getLaunchIntentForPackage(packageName) + if (relaunchIntent != null) { + relaunchIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK or android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(relaunchIntent) + android.os.Process.killProcess(android.os.Process.myPid()) + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index bdc51b7070..71a3e54cb3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -425,6 +425,14 @@ class SettingsAdapter( position ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) + // reset language if detected + if (item.setting.key == "app_language") { + // recreate page apply language change instantly + fragment.requireActivity().recreate() + + settingsViewModel.setShouldRecreateForLanguageChange(true) + } + return true } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt index eb1b0a4257..51d0455fd5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt @@ -387,6 +387,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener .show() } scSetting.setSelectedValue(value) + + if (scSetting.setting.key == "app_language") { + settingsViewModel.setShouldRecreateForLanguageChange(true) + // recreate page apply language change instantly + requireActivity().recreate() + } } is StringSingleChoiceSetting -> { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 8a0bea158e..b495206bb2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1030,8 +1030,10 @@ class SettingsFragmentPresenter( override fun reset() = IntSetting.THEME.setInt(defaultValue) } + add(HeaderSetting(R.string.app_settings)) + add(IntSetting.APP_LANGUAGE.key) + if (NativeLibrary.isUpdateCheckerEnabled()) { - add(HeaderSetting(R.string.app_settings)) add(BooleanSetting.ENABLE_UPDATE_CHECKS.key) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt index fbdca04e9c..d47e33244e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -54,6 +57,8 @@ class SettingsViewModel : ViewModel() { private val _shouldShowResetInputDialog = MutableStateFlow(false) val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow() + private val _shouldRecreateForLanguageChange = MutableStateFlow(false) + val shouldRecreateForLanguageChange = _shouldRecreateForLanguageChange.asStateFlow() fun setShouldRecreate(value: Boolean) { _shouldRecreate.value = value } @@ -103,6 +108,10 @@ class SettingsViewModel : ViewModel() { _shouldShowResetInputDialog.value = value } + fun setShouldRecreateForLanguageChange(value: Boolean) { + _shouldRecreateForLanguageChange.value = value + } + fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage = try { InputHandler.registeredControllers[currentDevice] diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 126d85d715..da790a4fc4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -4,6 +4,7 @@ package org.yuzu.yuzu_emu.ui.main import android.content.Intent +import android.content.Context import android.net.Uri import android.os.Bundle import android.view.View @@ -53,6 +54,7 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity import kotlin.text.compareTo import androidx.core.net.toUri import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting +import org.yuzu.yuzu_emu.YuzuApplication class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding @@ -68,6 +70,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private val CHECKED_DECRYPTION = "CheckedDecryption" private var checkedDecryption = false + override fun attachBaseContext(base: Context) { + super.attachBaseContext(YuzuApplication.applyLanguage(base)) + } + override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index 53fdab8b4a..8a46a22df1 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -62,6 +62,7 @@ namespace AndroidSettings { Settings::Setting theme_mode{linkage, -1, "theme_mode", Settings::Category::Android}; Settings::Setting black_backgrounds{linkage, false, "black_backgrounds", Settings::Category::Android}; + Settings::Setting app_language{linkage, 0, "app_language", Settings::Category::Android}; Settings::Setting enable_update_checks{linkage, true, "enable_update_checks", Settings::Category::Android}; diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 3cd1e1d0ab..12dec24219 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -391,6 +391,61 @@ 2 + + @string/app_language_system + @string/app_language_english + @string/app_language_spanish + @string/app_language_french + @string/app_language_german + @string/app_language_italian + @string/app_language_portuguese + @string/app_language_brazilian_portuguese + @string/app_language_russian + @string/app_language_japanese + @string/app_language_korean + @string/app_language_simplified_chinese + @string/app_language_traditional_chinese + @string/app_language_polish + @string/app_language_czech + @string/app_language_norwegian + @string/app_language_hungarian + @string/app_language_ukrainian + @string/app_language_vietnamese + @string/app_language_indonesian + @string/app_language_arabic + @string/app_language_central_kurdish + @string/app_language_persian + @string/app_language_hebrew + @string/app_language_serbian + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + + @string/auto @string/oboe diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 03f546333a..fb80eaf053 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1028,6 +1028,35 @@ Black backgrounds When using the dark theme, apply black backgrounds. + + App Language + Change the language of the app interface + Follow System + English + Español + Français + Deutsch + Italiano + Português + Português do Brasil + Русский + 日本語 + 한국어 + 简体中文 + 繁體中文 + Polski + Čeština + Norsk bokmål + Magyar + Українська + Tiếng Việt + Bahasa Indonesia + العربية + کوردیی ناوەندی + فارسی + עברית + Српски + Theme Color Eden (Default)