Skip to content

Commit

Permalink
Merge pull request #324 from CodeRedDev/feature/per-app-language
Browse files Browse the repository at this point in the history
Per App Language
  • Loading branch information
G4m813R authored Feb 6, 2025
2 parents 53229e6 + b692723 commit 8d30b71
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 3 deletions.
13 changes: 13 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ android {
buildFeatures {
viewBinding true
buildConfig true
compose true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.13"
}

buildTypes {
Expand Down Expand Up @@ -194,6 +199,14 @@ dependencies {

implementation 'androidx.fragment:fragment-ktx:1.6.2'

// Compose
def composeBom = platform('androidx.compose:compose-bom:2025.01.00')
implementation composeBom
implementation 'androidx.compose.material:material'
// Preview support
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'

// 23.0.0 to 23.2.0 breaks the apk for some reason (too small, fails to read android-manifest)
implementation "com.google.android.gms:play-services-ads:22.6.0"

Expand Down
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
android:roundIcon="@mipmap/ic_roundapp"
android:supportsRtl="false"
android:theme="@style/AppTheme.Orange.NoActionBar"
android:localeConfig="@xml/locales_config"
tools:replace="android:allowBackup,android:supportsRtl">

<uses-library
Expand Down Expand Up @@ -100,6 +101,17 @@
android:authorities="${applicationId}.firebaseinitprovider"
tools:node="remove" />

<!-- Add support for per-app language below Android 12 (API 32) -->
<!-- This can lead to violations in StrictMode (see docs) -->
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>

<activity
android:name=".ui.MainActivity"
android:label="@string/app_name"
Expand Down
33 changes: 31 additions & 2 deletions app/src/main/java/com/pr0gramm/app/ui/SettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@ package com.pr0gramm.app.ui
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.DrawableCompat
import androidx.preference.Preference
import androidx.preference.PreferenceGroup
import com.pr0gramm.app.*
import com.pr0gramm.app.services.*
import com.pr0gramm.app.BuildConfig
import com.pr0gramm.app.Instant
import com.pr0gramm.app.R
import com.pr0gramm.app.RequestCodes
import com.pr0gramm.app.Settings
import com.pr0gramm.app.services.BookmarkService
import com.pr0gramm.app.services.RecentSearchesServices
import com.pr0gramm.app.services.Storage
import com.pr0gramm.app.services.ThemeHelper
import com.pr0gramm.app.services.UserService
import com.pr0gramm.app.services.preloading.PreloadManager
import com.pr0gramm.app.ui.base.BaseAppCompatActivity
import com.pr0gramm.app.ui.base.BasePreferenceFragment
import com.pr0gramm.app.ui.base.launchUntilPause
import com.pr0gramm.app.ui.base.launchWhenStarted
import com.pr0gramm.app.ui.dialogs.LanguagePickerDialog
import com.pr0gramm.app.ui.dialogs.UpdateDialogFragment
import com.pr0gramm.app.ui.intro.IntroActivity
import com.pr0gramm.app.util.AndroidUtility
Expand Down Expand Up @@ -52,6 +63,11 @@ class SettingsFragment : BasePreferenceFragment("SettingsFragment"),
hidePreferenceByName("pref_pseudo_restore_bookmarks")
}

// Per-app language works for API 24+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
hidePreferenceByName("pref_pseudo_language")
}

tintPreferenceIcons(color = 0xffd0d0d0.toInt())
}

Expand Down Expand Up @@ -184,6 +200,19 @@ class SettingsFragment : BasePreferenceFragment("SettingsFragment"),
return true
}

"pref_pseudo_language" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val intent = Intent(
android.provider.Settings.ACTION_APP_LOCALE_SETTINGS,
Uri.parse("package:${requireContext().packageName}"),
)
startActivity(intent)
} else {
LanguagePickerDialog().show(parentFragmentManager, null)
}
return true
}

else -> return super.onPreferenceTreeClick(preference)
}
}
Expand Down
217 changes: 217 additions & 0 deletions app/src/main/java/com/pr0gramm/app/ui/dialogs/LanguagePickerDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package com.pr0gramm.app.ui.dialogs

import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonDefaults.textButtonColors
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.os.LocaleListCompat
import com.pr0gramm.app.R
import com.pr0gramm.app.ui.base.BaseDialogFragment
import org.xmlpull.v1.XmlPullParser
import java.util.Locale

class LanguagePickerDialog : BaseDialogFragment("LanguagePickerDialog") {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val supportedLocales = getSupportedLocales()
val currentLocale = getCurrentLocale(supportedLocales)

return ComposeView(requireContext()).apply {
setContent {
DialogContent(supportedLocales, currentLocale)
}
}
}

@Composable
fun DialogContent(
supportedLocales: List<Locale>,
initialLocale: Locale
) {
val selectedLocale = remember { mutableStateOf(initialLocale) }

Dialog(
onDismissRequest = { dismiss() }
) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
val titleStyle = MaterialTheme.typography.subtitle1
val subtitleStyle = MaterialTheme.typography.body2
Column {
ProvideTextStyle(titleStyle) {
Text(
stringResource(R.string.language_picker_title),
Modifier
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
.align(Alignment.Start),
)
}
ProvideTextStyle(subtitleStyle) {
Text(
stringResource(R.string.language_picker_subtitle),
Modifier
.padding(vertical = 4.dp, horizontal = 16.dp)
.align(Alignment.Start),
)
}
supportedLocales.forEach {
LanguageItem(
it,
selected = it == selectedLocale.value,
onClick = {
selectedLocale.value = it
},
)
}
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(
onClick = { dismiss() },
modifier = Modifier.padding(8.dp),
colors = textButtonColors(
contentColor = colorResource(R.color.orange_primary)
),
) {
Text(stringResource(R.string.language_picker_dismiss))
}
TextButton(
onClick = {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.create(
selectedLocale.value
)
)
dismiss()
},
modifier = Modifier.padding(8.dp),
colors = textButtonColors(
contentColor = colorResource(R.color.orange_primary)
),
) {
Text(stringResource(R.string.language_picker_confirm))
}
}
}
}
}
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun LanguageItem(
locale: Locale,
selected: Boolean,
onClick: (() -> Unit),
) {
ListItem(
modifier = Modifier.clickable {
onClick()
},
text = { Text(locale.getDisplayLanguage(locale)) },
trailing = {
RadioButton(
selected = selected,
onClick = onClick,
colors = RadioButtonDefaults.colors(
selectedColor = colorResource(R.color.orange_primary)
)
)
}
)
}

private fun getSupportedLocales(): List<Locale> {
val locales = mutableListOf<Locale>()

try {
val parser = resources.getXml(R.xml.locales_config)
var eventType = parser.eventType
val namespace = "http://schemas.android.com/apk/res/android"

while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
val languageTag = parser.getAttributeValue(namespace, "name")
if (languageTag != null) {
val locale = Locale.forLanguageTag(languageTag)
locales.add(locale)
}
}
eventType = parser.next()
}

parser.close()
} catch (e: Exception) {
logger.error("Error parsing locales config!", e)
}

return locales
}

private fun getCurrentLocale(supportedLocales: List<Locale>): Locale {
val applicationLocales = AppCompatDelegate.getApplicationLocales()
val appLocale = if (!applicationLocales.isEmpty) {
applicationLocales.get(0)!!
} else {
getCurrentAppLocale()
}

val candidate = supportedLocales.firstOrNull { it.isO3Language == appLocale.isO3Language }

if (candidate != null) {
return candidate
}

return Locale.forLanguageTag("en-US")
}

private fun getCurrentAppLocale(): Locale {
val config = requireContext().resources.configuration
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.locales[0]
} else {
@Suppress("DEPRECATION")
config.locale
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/com/pr0gramm/app/util/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ inline fun <K : Any, V : Any> LruCache<K, V>.getOrPut(key: K, creator: (K) -> V)
}
}

fun <K : Any, V> lruCache(maxSize: Int, creator: (K) -> V?): LruCache<K, V> {
fun <K : Any, V: Any> lruCache(maxSize: Int, creator: (K) -> V?): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun create(key: K) = creator(key)
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_white_translate.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>

</vector>
7 changes: 7 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -602,4 +602,11 @@
<string name="pref_show_category_junk_summary">Aktiviere diese Option um die \'Müll\' Kategorie in der Navigation zu zeigen.</string>
<string name="pref_feed_hide_junk_in_new_title">Mülltrennung</string>
<string name="pref_feed_hide_junk_in_new_summary">Deaktiviere diese Option um \'Müll\' auch in \'Neu\' anzuzeigen.</string>

<string name="pref_pseudo_language_title">Sprache</string>
<string name="pref_pseudo_language_summary">Ändere die Sprache der App unabhängig deiner Systemsprache</string>
<string name="language_picker_title">Sprache wählen</string>
<string name="language_picker_subtitle">Bitte wähle eine der verfügbaren Sprachen.</string>
<string name="language_picker_dismiss">Abbrechen</string>
<string name="language_picker_confirm">Bestätigen</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -622,5 +622,12 @@
<string name="pref_video_quality_values_title">Video quality</string>
<string name="action_feed_type_junk">Junk</string>
<string name="pref_show_category_junk_title">Category \'Junk\'</string>

<string name="pref_pseudo_language_title">Language</string>
<string name="pref_pseudo_language_summary">Change the app language independent of your system language</string>
<string name="language_picker_title">Choose language</string>
<string name="language_picker_subtitle">Please choose one of the available languages.</string>
<string name="language_picker_dismiss">Dismiss</string>
<string name="language_picker_confirm">Confirm</string>
</resources>

5 changes: 5 additions & 0 deletions app/src/main/res/xml/locales_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en-US"/>
<locale android:name="de-DE"/>
</locale-config>
6 changes: 6 additions & 0 deletions app/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@

</PreferenceScreen>

<Preference
android:icon="@drawable/ic_white_translate"
android:key="pref_pseudo_language"
android:summary="@string/pref_pseudo_language_summary"
android:title="@string/pref_pseudo_language_title" />

</PreferenceCategory>

<!-- This screen is hidden on non debug builds -->
Expand Down

0 comments on commit 8d30b71

Please sign in to comment.