mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-18 08:43:56 +00:00
feat: Add language settings (#2913)
This commit is contained in:
43
.github/workflows/pull_strings.yml
vendored
Normal file
43
.github/workflows/pull_strings.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Pull strings
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 0"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull:
|
||||||
|
name: Pull strings
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: dev
|
||||||
|
clean: true
|
||||||
|
|
||||||
|
- name: Pull strings
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
config: crowdin.yml
|
||||||
|
upload_sources: false
|
||||||
|
download_translations: true
|
||||||
|
skip_ref_checkout: true
|
||||||
|
localization_branch_name: feat/translations
|
||||||
|
create_pull_request: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
|
||||||
|
- name: Open pull request
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
uses: repo-sync/pull-request@v2
|
||||||
|
with:
|
||||||
|
source_branch: feat/translations
|
||||||
|
destination_branch: dev
|
||||||
|
pr_title: "chore: Sync translations"
|
||||||
|
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
||||||
26
.github/workflows/push_strings.yml
vendored
Normal file
26
.github/workflows/push_strings.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Push strings
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- app/src/main/res/values/strings.xml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push:
|
||||||
|
name: Push strings
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Push strings
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
config: crowdin.yml
|
||||||
|
upload_sources: true
|
||||||
|
env:
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
@@ -149,7 +149,6 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||||
isPseudoLocalesEnabled = true
|
|
||||||
|
|
||||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
}
|
}
|
||||||
@@ -243,6 +242,8 @@ android {
|
|||||||
version = "3.22.1"
|
version = "3.22.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets["main"].kotlin.srcDir(layout.buildDirectory.dir("generated/source/locales"))
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -250,6 +251,47 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
val generateSupportedLocales by registering {
|
||||||
|
description = "Generate list of supported locales from resource directories"
|
||||||
|
|
||||||
|
val resDir = file("src/main/res")
|
||||||
|
val outputDir = layout.buildDirectory.dir("generated/source/locales")
|
||||||
|
|
||||||
|
inputs.dir(resDir)
|
||||||
|
outputs.dir(outputDir)
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val locales = resDir.listFiles()
|
||||||
|
.orEmpty()
|
||||||
|
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
|
||||||
|
.map { it.name.removePrefix("values-").replace("-r", "-") }
|
||||||
|
.sorted()
|
||||||
|
.joinToString(",\n ") { "Locale.forLanguageTag(\"$it\")" }
|
||||||
|
|
||||||
|
val output = outputDir.get().asFile.resolve("app/revanced/manager/util/GeneratedLocales.kt")
|
||||||
|
output.parentFile.mkdirs()
|
||||||
|
output.writeText(
|
||||||
|
"""
|
||||||
|
|package app.revanced.manager.util
|
||||||
|
|
|
||||||
|
|import java.util.Locale
|
||||||
|
|
|
||||||
|
|object GeneratedLocales {
|
||||||
|
| val SUPPORTED_LOCALES = listOf(
|
||||||
|
| Locale.ENGLISH,
|
||||||
|
| $locales,
|
||||||
|
| )
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preBuild {
|
||||||
|
dependsOn(generateSupportedLocales)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Needed by gradle-semantic-release-plugin.
|
// Needed by gradle-semantic-release-plugin.
|
||||||
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||||
val publish by registering {
|
val publish by registering {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package app.revanced.manager
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
@@ -63,7 +63,7 @@ import org.koin.androidx.compose.navigation.koinNavViewModel
|
|||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
@@ -19,6 +21,7 @@ import androidx.compose.material3.rememberTopAppBarState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -38,6 +41,7 @@ import app.revanced.manager.ui.theme.Theme
|
|||||||
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -48,6 +52,7 @@ fun GeneralSettingsScreen(
|
|||||||
val prefs = viewModel.prefs
|
val prefs = viewModel.prefs
|
||||||
val coroutineScope = viewModel.viewModelScope
|
val coroutineScope = viewModel.viewModelScope
|
||||||
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showThemePicker) {
|
if (showThemePicker) {
|
||||||
ThemePicker(
|
ThemePicker(
|
||||||
@@ -55,6 +60,17 @@ fun GeneralSettingsScreen(
|
|||||||
onConfirm = { viewModel.setTheme(it) }
|
onConfirm = { viewModel.setTheme(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showLanguagePicker) {
|
||||||
|
LanguagePicker(
|
||||||
|
supportedLocales = viewModel.getSupportedLocales(),
|
||||||
|
currentLocale = viewModel.getCurrentLocale(),
|
||||||
|
onDismiss = { showLanguagePicker = false },
|
||||||
|
onConfirm = { viewModel.setLocale(it) },
|
||||||
|
getDisplayName = { viewModel.getLocaleDisplayName(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -74,6 +90,24 @@ fun GeneralSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
GroupHeader(stringResource(R.string.appearance))
|
GroupHeader(stringResource(R.string.appearance))
|
||||||
|
|
||||||
|
val currentLocale = viewModel.getCurrentLocale()
|
||||||
|
val currentLanguageDisplay = remember(currentLocale) {
|
||||||
|
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
|
||||||
|
}
|
||||||
|
SettingsListItem(
|
||||||
|
modifier = Modifier.clickable { showLanguagePicker = true },
|
||||||
|
headlineContent = stringResource(R.string.language),
|
||||||
|
supportingContent = stringResource(R.string.language_description),
|
||||||
|
trailingContent = {
|
||||||
|
FilledTonalButton(onClick = { showLanguagePicker = true }) {
|
||||||
|
Text(
|
||||||
|
currentLanguageDisplay
|
||||||
|
?: stringResource(R.string.language_system_default)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val theme by prefs.theme.getAsState()
|
val theme by prefs.theme.getAsState()
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { showThemePicker = true },
|
modifier = Modifier.clickable { showThemePicker = true },
|
||||||
@@ -157,3 +191,63 @@ private fun ThemePicker(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LanguagePicker(
|
||||||
|
supportedLocales: List<Locale>,
|
||||||
|
currentLocale: Locale?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (Locale?) -> Unit,
|
||||||
|
getDisplayName: (Locale) -> String
|
||||||
|
) {
|
||||||
|
var selectedLocale by remember { mutableStateOf(currentLocale) }
|
||||||
|
val systemDefaultString = stringResource(R.string.language_system_default)
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.language)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { selectedLocale = null },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HapticRadioButton(
|
||||||
|
selected = selectedLocale == null,
|
||||||
|
onClick = { selectedLocale = null }
|
||||||
|
)
|
||||||
|
Text(systemDefaultString)
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedLocales.forEach { locale ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { selectedLocale = locale },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HapticRadioButton(
|
||||||
|
selected = selectedLocale == locale,
|
||||||
|
onClick = { selectedLocale = locale }
|
||||||
|
)
|
||||||
|
Text(getDisplayName(locale))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onConfirm(selectedLocale)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.apply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
import app.revanced.manager.util.SupportedLocales
|
||||||
import app.revanced.manager.util.resetListItemColorsCached
|
import app.revanced.manager.util.resetListItemColorsCached
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class GeneralSettingsViewModel(
|
class GeneralSettingsViewModel(
|
||||||
|
private val app: Application,
|
||||||
val prefs: PreferencesManager
|
val prefs: PreferencesManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||||
prefs.theme.update(theme)
|
prefs.theme.update(theme)
|
||||||
resetListItemColorsCached()
|
resetListItemColorsCached()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
|
||||||
|
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
|
||||||
|
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
|
||||||
|
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package app.revanced.manager.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.LocaleList
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object SupportedLocales {
|
||||||
|
fun getSupportedLocales(context: Context): List<Locale> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
runCatching {
|
||||||
|
android.app.LocaleConfig(context).supportedLocales?.toList()
|
||||||
|
}.getOrNull() ?: GeneratedLocales.SUPPORTED_LOCALES
|
||||||
|
} else {
|
||||||
|
GeneratedLocales.SUPPORTED_LOCALES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentLocale(): Locale? =
|
||||||
|
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
|
||||||
|
|
||||||
|
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
|
||||||
|
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getDisplayName(locale: Locale) =
|
||||||
|
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
|
||||||
|
|
||||||
|
private fun LocaleList.toList() = (0 until size()).map { get(it) }
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ Second \"item\" text"</string>
|
|||||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||||
|
|
||||||
<string name="general">General</string>
|
<string name="general">General</string>
|
||||||
<string name="general_description">Theme, dynamic color</string>
|
<string name="general_description">Language, theme, dynamic color</string>
|
||||||
<string name="updates">Updates</string>
|
<string name="updates">Updates</string>
|
||||||
<string name="updates_description">Check for updates and view changelogs</string>
|
<string name="updates_description">Check for updates and view changelogs</string>
|
||||||
<string name="downloads">Downloads</string>
|
<string name="downloads">Downloads</string>
|
||||||
@@ -104,6 +104,9 @@ Second \"item\" text"</string>
|
|||||||
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
||||||
<string name="theme">Theme</string>
|
<string name="theme">Theme</string>
|
||||||
<string name="theme_description">Choose between light or dark theme</string>
|
<string name="theme_description">Choose between light or dark theme</string>
|
||||||
|
<string name="language">Language</string>
|
||||||
|
<string name="language_description">Choose the app display language</string>
|
||||||
|
<string name="language_system_default">System default</string>
|
||||||
<string name="safeguards">Safeguards</string>
|
<string name="safeguards">Safeguards</string>
|
||||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||||
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
||||||
|
|||||||
8
crowdin.yml
Normal file
8
crowdin.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
project_id_env: "CROWDIN_PROJECT_ID"
|
||||||
|
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
||||||
|
|
||||||
|
preserve_hierarchy: false
|
||||||
|
files:
|
||||||
|
- source: app/src/main/res/values/strings.xml
|
||||||
|
translation: app/src/main/res/values-%android_code%/strings.xml
|
||||||
|
skip_untranslated_strings: true
|
||||||
Reference in New Issue
Block a user