diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java new file mode 100644 index 000000000..e6d029a2e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java @@ -0,0 +1,114 @@ +package app.revanced.extension.shared.settings; + +import java.util.Locale; + +public enum AppLanguage { + /** + * The current app language. + */ + DEFAULT, + + // Language codes found in locale_config.xml + // All region specific variants have been removed. + AF, + AM, + AR, + AS, + AZ, + BE, + BG, + BN, + BS, + CA, + CS, + DA, + DE, + EL, + EN, + ES, + ET, + EU, + FA, + FI, + FR, + GL, + GU, + HI, + HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code. + HR, + HU, + HY, + ID, + IS, + IT, + JA, + KA, + KK, + KM, + KN, + KO, + KY, + LO, + LT, + LV, + MK, + ML, + MN, + MR, + MS, + MY, + NE, + NL, + NB, + OR, + PA, + PL, + PT, + RO, + RU, + SI, + SK, + SL, + SQ, + SR, + SV, + SW, + TA, + TE, + TH, + TL, + TR, + UK, + UR, + UZ, + VI, + ZH, + ZU; + + private final String language; + + AppLanguage() { + language = name().toLowerCase(Locale.US); + } + + /** + * @return The 2 letter ISO 639_1 language code. + */ + public String getLanguage() { + // Changing the app language does not force the app to completely restart, + // so the default needs to be the current language and not a static field. + if (this == DEFAULT) { + return Locale.getDefault().getLanguage(); + } + + return language; + } + + public Locale getLocale() { + if (this == DEFAULT) { + return Locale.getDefault(); + } + + return Locale.forLanguageTag(language); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index c97e3f87c..2044089f1 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -22,6 +22,8 @@ public class BaseSettings { public static final BooleanSetting ENABLE_DEBUG_BUFFER_LOGGING = new BooleanSetting("revanced_enable_debug_buffer_logging", FALSE); public static final BooleanSetting SETTINGS_INITIALIZED = new BooleanSetting("revanced_settings_initialized", FALSE, false, false); + public static final EnumSetting REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true); + /** * These settings are used by YouTube and YouTube Music. */ diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java index e1290d73f..5ef12c4a8 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -43,6 +43,8 @@ import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import app.revanced.extension.shared.settings.AppLanguage; +import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; import kotlin.text.Regex; @@ -280,14 +282,13 @@ public class Utils { } public static Resources getResources() { + if (context != null) { + return context.getResources(); + } Activity mActivity = activityRef.get(); if (mActivity != null) { return mActivity.getResources(); } - Context mContext = getContext(); - if (mContext != null) { - return mContext.getResources(); - } throw new IllegalStateException("Get resources failed"); } @@ -301,7 +302,7 @@ public class Utils { * @param mContext Context to check locale. * @return Context with locale applied. */ - public static Context getLocalizedContextAndSetResources(Context mContext) { + public static Context getLocalizedContext(Context mContext) { Activity mActivity = activityRef.get(); if (mActivity == null) { return mContext; @@ -310,19 +311,15 @@ public class Utils { return null; } - // Locale of MainActivity. - Locale applicationLocale; + AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get(); + + // Locale of Application. + Locale applicationLocale = language == AppLanguage.DEFAULT + ? mActivity.getResources().getConfiguration().locale + : language.getLocale(); // Locale of Context. - Locale contextLocale; - - if (isSDKAbove(24)) { - applicationLocale = mActivity.getResources().getConfiguration().getLocales().get(0); - contextLocale = mContext.getResources().getConfiguration().getLocales().get(0); - } else { - applicationLocale = mActivity.getResources().getConfiguration().locale; - contextLocale = mContext.getResources().getConfiguration().locale; - } + Locale contextLocale = mContext.getResources().getConfiguration().locale; // If they are identical, no need to override them. if (applicationLocale == contextLocale) { @@ -350,6 +347,14 @@ public class Utils { context = appContext; + AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get(); + if (language != AppLanguage.DEFAULT) { + // Create a new context with the desired language. + Configuration config = appContext.getResources().getConfiguration(); + config.setLocale(language.getLocale()); + context = appContext.createConfigurationContext(config); + } + // In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies. // Calling the regular printDebug method here can cause a Settings context null pointer exception, // even though the context is already set before the call. diff --git a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java index 14d19be64..64b06fcab 100644 --- a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java +++ b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java @@ -44,7 +44,7 @@ public class VideoQualitySettingsActivity extends Activity { @Override protected void attachBaseContext(Context base) { - super.attachBaseContext(Utils.getLocalizedContextAndSetResources(base)); + super.attachBaseContext(Utils.getLocalizedContext(base)); } @Override diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt index 3c9dc5ee8..f7f3b8b97 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt @@ -49,6 +49,7 @@ fun ResourcePatchContext.baseTranslationsPatch( sourceDirectory: String, ) { val resourceDirectory = get("res") + val isYouTube = sourceDirectory == "youtube" // Check if the custom translation path is valid. customTranslations?.takeIf { it.isNotEmpty() }?.let { customLang -> @@ -90,10 +91,10 @@ fun ResourcePatchContext.baseTranslationsPatch( // Filter the app languages to include both versions of locales (with and without 'r', en-rGB and en-GB) // and also handle locales with "b+" prefix - val filteredAppLanguages = selectedStringResourcesArray.flatMap { language -> - setOf(language, language.replace("-r", "-"), - language.replace("b+", "").replace("+", "-")) - }.toTypedArray() + var filteredAppLanguages = (selectedStringResourcesArray + arrayOf("en")) + .map { language -> + language.replace("-r", "-").replace("b+", "").replace("+", "-") + }.toHashSet().toTypedArray() // Remove unselected app languages from UI document("res/xml/locales_config.xml").use { document -> @@ -102,7 +103,7 @@ fun ResourcePatchContext.baseTranslationsPatch( document.doRecursively { node -> if (node is Element && node.tagName == "locale") { node.getAttributeNode("android:name")?.let { attribute -> - if (attribute.textContent != "en" && attribute.textContent !in filteredAppLanguages) { + if (attribute.textContent !in filteredAppLanguages) { nodesToRemove.add(node) } } @@ -114,6 +115,45 @@ fun ResourcePatchContext.baseTranslationsPatch( node.parentNode?.removeChild(node) } } + + if (!isYouTube) return + + filteredAppLanguages = filteredAppLanguages.map { language -> + language.subSequence(0,2).toString().uppercase() + }.toHashSet().toTypedArray() + + // Remove unselected app languages from RVX Settings + setOf( + "revanced_language_entries", + "revanced_language_entry_values", + ).forEach { attributeName -> + document("res/values/arrays.xml").use { document -> + with(document) { + val nodesToRemove = mutableListOf() + + val resourcesNode = getElementsByTagName("resources").item(0) as Element + for (i in 0 until resourcesNode.childNodes.length) { + val node = resourcesNode.childNodes.item(i) as? Element ?: continue + + if (node.getAttribute("name") == attributeName) { + for (j in 0 until node.childNodes.length) { + val item = node.childNodes.item(j) as? Element ?: continue + val text = item.textContent + val length = text.length + if (!text.endsWith("DEFAULT") && text.subSequence(length - 2, length) !in filteredAppLanguages) { + nodesToRemove.add(item) + } + } + } + } + + // Remove the collected nodes (avoids NullPointerException) + for (n in nodesToRemove) { + n.parentNode?.removeChild(n) + } + } + } + } } /** diff --git a/patches/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml index 55d4d5583..59dee4e88 100644 --- a/patches/src/main/resources/youtube/settings/host/values/arrays.xml +++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml @@ -183,6 +183,114 @@ MEMBERSHIPS_SHORTS_ONLY MEMBERSHIPS_LIVESTREAMS_ONLY + + @string/revanced_language_DEFAULT + @string/revanced_language_AR + @string/revanced_language_AZ + @string/revanced_language_BG + @string/revanced_language_BN + @string/revanced_language_CA + @string/revanced_language_CS + @string/revanced_language_DA + @string/revanced_language_DE + @string/revanced_language_EL + @string/revanced_language_EN + @string/revanced_language_ES + @string/revanced_language_ET + @string/revanced_language_FA + @string/revanced_language_FI + @string/revanced_language_FR + @string/revanced_language_GU + @string/revanced_language_HI + @string/revanced_language_HR + @string/revanced_language_HU + @string/revanced_language_ID + @string/revanced_language_IT + @string/revanced_language_JA + @string/revanced_language_KK + @string/revanced_language_KO + @string/revanced_language_LT + @string/revanced_language_LV + @string/revanced_language_MK + @string/revanced_language_MN + @string/revanced_language_MR + @string/revanced_language_MS + @string/revanced_language_MY + @string/revanced_language_NL + @string/revanced_language_OR + @string/revanced_language_PA + @string/revanced_language_PL + @string/revanced_language_PT + @string/revanced_language_RO + @string/revanced_language_RU + @string/revanced_language_SK + @string/revanced_language_SL + @string/revanced_language_SR + @string/revanced_language_SV + @string/revanced_language_SW + @string/revanced_language_TA + @string/revanced_language_TE + @string/revanced_language_TH + @string/revanced_language_TR + @string/revanced_language_UK + @string/revanced_language_UR + @string/revanced_language_VI + @string/revanced_language_ZH + + + DEFAULT + AR + AZ + BG + BN + CA + CS + DA + DE + EL + EN + ES + ET + FA + FI + FR + GU + HI + HR + HU + ID + IT + JA + KK + KO + LT + LV + MK + MN + MR + MS + MY + NL + OR + PA + PL + PT + RO + RU + SK + SL + SR + SV + SW + TA + TE + TH + TR + UK + UR + VI + ZH + @string/revanced_miniplayer_type_entry_0 @string/revanced_miniplayer_type_entry_1 diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml index 6ed8b4f25..a6ced114b 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -22,6 +22,59 @@ "%1$s is not installed. Please download %2$s from the website." %s is not installed. Please install it. + RVX language + App language + Arabic + Azerbaijani + Bulgarian + Bengali + Catalan + Czech + Danish + German + Greek + English + Spanish + Estonian + Persian + Finnish + French + Gujarati + Hindi + Croatian + Hungarian + Indonesian + Italian + Japanese + Kazakh + Korean + Lithuanian + Latvian + Macedonian + Mongolian + Marathi + Malay + Burmese + Dutch + Odia + Punjabi + Polish + Portuguese + Romanian + Russian + Slovak + Slovene + Serbian + Swedish + Swahili + Tamil + Telugu + Thai + Turkish + Ukrainian + Urdu + Vietnamese + Chinese diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index f83a55c62..8c4324e90 100644 --- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -838,6 +838,8 @@ PREFERENCE: GMS_CORE_SETTINGS --> + +