feat: i18n + crowdin (#31)

This commit is contained in:
rushii 2022-10-22 17:56:14 -07:00 • committed by GitHub
parent 6965f3128b
commit e7e5193dd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 315 additions and 160 deletions

42
.github/workflows/crowdin.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Sync Crowdin
on:
push:
branches:
- 'main'
paths:
- 'app/src/main/res/values/strings.xml'
- '.github/workflows/crowdin.yml'
schedule:
- cron: "0 17 * * 6" # "At 17:00 on Saturday."
workflow_dispatch:
jobs:
sync-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Crowdin
uses: crowdin/github-action@1.4.11
with:
config: crowdin.yml
upload_translations: true
download_translations: true
push_translations: true
create_pull_request: false
localization_branch_name: l10n
commit_message: 'chore(i18n): sync translations'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
- name: Merge
run: |
sudo chmod -R ugo+rwX .
git checkout main
git add *
git merge l10n
git push

View file

@ -7,6 +7,7 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
class InstallService : Service() { class InstallService : Service() {
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
@ -21,19 +22,19 @@ class InstallService : Service() {
PackageInstaller.STATUS_SUCCESS -> Toast.makeText( PackageInstaller.STATUS_SUCCESS -> Toast.makeText(
this, this,
"Successfully installed Aliucord!", getString(R.string.install_success),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText( PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText(
this, this,
"Aborted installation", getString(R.string.install_aborted),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
else -> { else -> {
Log.i(BuildConfig.TAG, "Installation failed: error code $statusCode") Log.i(BuildConfig.TAG, "Install failed with error code $statusCode")
Toast.makeText(this, "Failed: Error $statusCode", Toast.LENGTH_LONG).show() Toast.makeText(this, getString(R.string.install_failed, statusCode), Toast.LENGTH_LONG).show()
} }
} }

View file

@ -79,7 +79,7 @@ fun ContributorEntry(user: GithubUser) {
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
Text( Text(
text = stringResource(R.string.contributions, user.contributions), text = stringResource(R.string.contributors_contributions, user.contributions),
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
) )

View file

@ -22,7 +22,7 @@ fun LoadFailure(onRetry: () -> Unit) {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = stringResource(R.string.load_fail), text = stringResource(R.string.network_load_fail),
style = MaterialTheme.typography.labelLarge.copy(fontSize = 16.sp), style = MaterialTheme.typography.labelLarge.copy(fontSize = 16.sp),
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
@ -30,7 +30,7 @@ fun LoadFailure(onRetry: () -> Unit) {
onClick = onRetry, onClick = onRetry,
shape = MaterialTheme.shapes.extraLarge shape = MaterialTheme.shapes.extraLarge
) { ) {
Text(text = stringResource(R.string.retry)) Text(text = stringResource(R.string.action_retry))
} }
} }
} }

View file

@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -23,6 +23,7 @@ import coil.compose.AsyncImage
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.network.dto.Commit import com.aliucord.manager.network.dto.Commit
import com.aliucord.manager.ui.component.LoadFailure import com.aliucord.manager.ui.component.LoadFailure
import com.aliucord.manager.ui.util.annotatingStringResource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Composable @Composable
@ -137,36 +138,27 @@ fun CommitItem(
model = "https://github.com/${commit.author.name}.png", model = "https://github.com/${commit.author.name}.png",
contentDescription = commit.author.name contentDescription = commit.author.name
) )
Text( Text(
buildAnnotatedString { annotatingStringResource(
withStyle( R.string.contributors_commit_title,
SpanStyle( commit.author.name,
commit.sha.take(8)
) {
when (it) {
"name" -> SpanStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
) { "middle" -> SpanStyle(
append(commit.author.name) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
}
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
)
) )
) { "commit" -> SpanStyle(
append(" authored ")
}
withStyle(
SpanStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
) { else -> null
append(commit.sha.substring(0, 7))
} }
}, },
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )

View file

@ -1,6 +1,8 @@
package com.aliucord.manager.ui.component.home package com.aliucord.manager.ui.component.home
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
@ -18,6 +20,7 @@ import com.aliucord.manager.R
fun InfoCard( fun InfoCard(
packageName: String, packageName: String,
supportedVersion: String, supportedVersion: String,
@StringRes supportedVersionType: Int,
currentVersion: String, currentVersion: String,
onDownloadClick: () -> Unit, onDownloadClick: () -> Unit,
onUninstallClick: () -> Unit, onUninstallClick: () -> Unit,
@ -31,22 +34,25 @@ fun InfoCard(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Text(
text = "Aliucord${packageName.let { if (it != "com.aliucord") " ($it)" else "" }}", text = "${stringResource(R.string.aliucord)} ($packageName)",
style = MaterialTheme.typography.titleLarge.copy(fontSize = 23.sp), style = MaterialTheme.typography.titleLarge.copy(fontSize = 23.sp),
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Text( Text(
buildAnnotatedString { buildAnnotatedString {
append("Supported version: ") append(stringResource(R.string.version_supported))
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(" ")
append(supportedVersion) append(supportedVersion)
append(" - ")
append(stringResource(supportedVersionType))
} }
append("\nInstalled version: ") append('\n')
append(stringResource(R.string.version_installed))
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(" ")
append(currentVersion) append(currentVersion)
} }
}, },
@ -61,9 +67,9 @@ fun InfoCard(
) { ) {
val (icon, description) = when (currentVersion) { val (icon, description) = when (currentVersion) {
"-" -> Icons.Default.Download to R.string.install "-" -> Icons.Default.Download to R.string.action_install
supportedVersion -> Icons.Default.Refresh to R.string.reinstall supportedVersion -> Icons.Default.Refresh to R.string.action_reinstall
else -> Icons.Default.Update to R.string.update else -> Icons.Default.Update to R.string.action_update
} }
FilledTonalIconButton( FilledTonalIconButton(
@ -98,7 +104,7 @@ fun InfoCard(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.uninstall) contentDescription = stringResource(R.string.action_uninstall)
) )
} }
@ -111,7 +117,7 @@ fun InfoCard(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Launch, imageVector = Icons.Default.Launch,
contentDescription = stringResource(R.string.launch) contentDescription = stringResource(R.string.action_launch)
) )
} }
} }

View file

@ -72,7 +72,7 @@ fun Changelog(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Default.History, imageVector = Icons.Default.History,
contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name) contentDescription = stringResource(R.string.plugins_view_changelog, plugin.manifest.name)
) )
}, },
title = { Text(plugin.manifest.name) }, title = { Text(plugin.manifest.name) },
@ -85,7 +85,7 @@ fun Changelog(
.heightIn(max = 90.dp) .heightIn(max = 90.dp)
.clip(RoundedCornerShape(14.dp)), .clip(RoundedCornerShape(14.dp)),
model = mediaUrl, model = mediaUrl,
contentDescription = stringResource(R.string.changelog_media) contentDescription = stringResource(R.string.plugins_changelog_media)
) )
} }
@ -156,7 +156,7 @@ fun Changelog(
}, },
confirmButton = { confirmButton = {
Button(onClick = onDismiss) { Button(onClick = onDismiss) {
Text(stringResource(R.string.close)) Text(stringResource(R.string.action_close))
} }
} }
) )

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.domain.model.Plugin import com.aliucord.manager.domain.model.Plugin
import com.aliucord.manager.ui.util.annotatingStringResource
import com.aliucord.manager.ui.util.joinToAnnotatedString import com.aliucord.manager.ui.util.joinToAnnotatedString
@Composable @Composable
@ -48,15 +49,19 @@ fun PluginCard(
Column { Column {
// Name // Name
Text( Text(
buildAnnotatedString { annotatingStringResource(
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { R.string.plugins_plugin_title,
append(plugin.manifest.name) plugin.manifest.name,
plugin.manifest.version
) {
when (it) {
"plugin" -> SpanStyle(fontWeight = FontWeight.Bold)
else -> null
} }
append(" v${plugin.manifest.version}")
} }
) )
// TODO: make this translatable
// Authors // Authors
val authors = buildAnnotatedString { val authors = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) { withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
@ -139,7 +144,7 @@ fun PluginCard(
Icon( Icon(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
imageVector = Icons.Default.History, imageVector = Icons.Default.History,
contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name) contentDescription = stringResource(R.string.plugins_view_changelog, plugin.manifest.name)
) )
} }
} }
@ -150,7 +155,7 @@ fun PluginCard(
Icon( Icon(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.uninstall), contentDescription = stringResource(R.string.action_uninstall),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error
) )
} }

View file

@ -31,16 +31,16 @@ fun InstallerDialog(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Default.Download, imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.download_method) contentDescription = stringResource(R.string.selector_download_method)
) )
}, },
title = { title = {
Text(stringResource(R.string.download_method)) Text(stringResource(R.string.selector_download_method))
}, },
text = { text = {
Column { Column {
mapOf( mapOf(
DownloadMethod.DOWNLOAD to stringResource(R.string.download_apk), DownloadMethod.DOWNLOAD to stringResource(R.string.selector_download_apk),
// DownloadMethod.SELECT to stringResource(R.string.select_apk) // DownloadMethod.SELECT to stringResource(R.string.select_apk)
).forEach { (method, name) -> ).forEach { (method, name) ->
Column { Column {
@ -75,7 +75,7 @@ fun InstallerDialog(
onDismissRequest() onDismissRequest()
} }
) { ) {
Text(stringResource(R.string.confirm)) Text(stringResource(R.string.action_confirm))
} }
}, },
dismissButton = { dismissButton = {
@ -86,7 +86,7 @@ fun InstallerDialog(
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer
) )
) { ) {
Text(stringResource(R.string.dismiss)) Text(stringResource(R.string.action_dismiss))
} }
}, },
onDismissRequest = onDismissRequest onDismissRequest = onDismissRequest

View file

@ -46,10 +46,10 @@ private fun ManageStorageDialog() {
.let { launcher.launch(it) } .let { launcher.launch(it) }
} }
) { ) {
Text(stringResource(R.string.open_settings)) Text(stringResource(R.string.action_open_settings))
} }
}, },
title = { Text(stringResource(R.string.permissions_grant)) }, title = { Text(stringResource(R.string.permissions_grant_title)) },
text = { Text(stringResource(R.string.permissions_grant_body)) }, text = { Text(stringResource(R.string.permissions_grant_body)) },
properties = DialogProperties( properties = DialogProperties(
dismissOnBackPress = false, dismissOnBackPress = false,
@ -69,10 +69,10 @@ private fun ExternalStorageDialog() {
onDismissRequest = {}, onDismissRequest = {},
confirmButton = { confirmButton = {
Button(onClick = writeStorageState::launchPermissionRequest) { Button(onClick = writeStorageState::launchPermissionRequest) {
Text(stringResource(R.string.confirm)) Text(stringResource(R.string.action_confirm))
} }
}, },
title = { Text(stringResource(R.string.permissions_grant)) }, title = { Text(stringResource(R.string.permissions_grant_title)) },
text = { Text(stringResource(R.string.permissions_grant_body)) }, text = { Text(stringResource(R.string.permissions_grant_body)) },
properties = DialogProperties( properties = DialogProperties(
dismissOnBackPress = false, dismissOnBackPress = false,

View file

@ -32,7 +32,7 @@ enum class HomeDestination(
val unselectedIcon: @RawValue ImageVector, val unselectedIcon: @RawValue ImageVector,
@StringRes val label: Int @StringRes val label: Int
) : Destination { ) : Destination {
HOME(Icons.Default.Home, Icons.Outlined.Home, R.string.home), HOME(Icons.Default.Home, Icons.Outlined.Home, R.string.navigation_home),
PLUGINS(Icons.Default.Extension, Icons.Outlined.Extension, R.string.plugins), PLUGINS(Icons.Default.Extension, Icons.Outlined.Extension, R.string.plugins_title),
SETTINGS(Icons.Default.Settings, Icons.Outlined.Settings, R.string.settings) SETTINGS(Icons.Default.Settings, Icons.Outlined.Settings, R.string.navigation_settings)
} }

View file

@ -43,12 +43,12 @@ fun AboutScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.about)) }, title = { Text(stringResource(R.string.navigation_about)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon( Icon(
imageVector = Icons.Default.ArrowBack, imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back) contentDescription = stringResource(R.string.navigation_back)
) )
} }
} }
@ -68,19 +68,19 @@ fun AboutScreen(
) { ) {
AsyncImage( AsyncImage(
model = "https://github.com/Aliucord.png", model = "https://github.com/Aliucord.png",
contentDescription = "Aliucord", contentDescription = stringResource(R.string.aliucord),
modifier = Modifier.size(71.dp) modifier = Modifier.size(71.dp)
) )
Text( Text(
text = "Aliucord", text = stringResource(R.string.aliucord),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
fontSize = 26.sp fontSize = 26.sp
) )
) )
Text( Text(
text = stringResource(R.string.aliucord_description), text = stringResource(R.string.app_description),
style = MaterialTheme.typography.titleSmall.copy( style = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
) )

View file

@ -45,7 +45,7 @@ fun MainRootScreen(
IconButton(onClick = onAboutClick) { IconButton(onClick = onAboutClick) {
Icon( Icon(
imageVector = Icons.Default.Info, imageVector = Icons.Default.Info,
contentDescription = stringResource(R.string.about) contentDescription = stringResource(R.string.navigation_about)
) )
} }
} }

View file

@ -57,6 +57,7 @@ fun HomeScreen(
InfoCard( InfoCard(
packageName = viewModel.preferences.packageName, packageName = viewModel.preferences.packageName,
supportedVersion = viewModel.supportedVersion, supportedVersion = viewModel.supportedVersion,
supportedVersionType = viewModel.supportedVersionType,
currentVersion = viewModel.installedVersion, currentVersion = viewModel.installedVersion,
onDownloadClick = { onDownloadClick = {
if (viewModel.preferences.devMode) { if (viewModel.preferences.devMode) {

View file

@ -45,12 +45,12 @@ fun InstallerScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("Installer") }, title = { Text(stringResource(R.string.installer)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon( Icon(
imageVector = Icons.Default.NavigateBefore, imageVector = Icons.Default.NavigateBefore,
contentDescription = stringResource(R.string.back) contentDescription = stringResource(R.string.navigation_back)
) )
} }
} }

View file

@ -40,14 +40,14 @@ fun PluginsScreen(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.uninstall) contentDescription = stringResource(R.string.action_uninstall)
) )
}, },
title = { title = {
Text("${stringResource(R.string.uninstall)} ${plugin.manifest.name}") Text("${stringResource(R.string.action_uninstall)} ${plugin.manifest.name}")
}, },
text = { text = {
Text(stringResource(R.string.delete_plugin_body)) Text(stringResource(R.string.plugins_delete_plugin_body))
}, },
confirmButton = { confirmButton = {
Button( Button(
@ -98,19 +98,19 @@ fun PluginsScreen(
onValueChange = viewModel::search, onValueChange = viewModel::search,
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
label = { Text(stringResource(R.string.search)) }, label = { Text(stringResource(R.string.action_search)) },
trailingIcon = { trailingIcon = {
if (viewModel.search.isNotBlank()) { if (viewModel.search.isNotBlank()) {
IconButton(onClick = viewModel::clearSearch) { IconButton(onClick = viewModel::clearSearch) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.clear) contentDescription = stringResource(R.string.action_clear)
) )
} }
} else { } else {
Icon( Icon(
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.search_plugins) contentDescription = stringResource(R.string.plugins_search)
) )
} }
}, },
@ -150,7 +150,7 @@ fun PluginsScreen(
imageVector = Icons.Default.Extension, imageVector = Icons.Default.Extension,
contentDescription = null contentDescription = null
) )
Text(stringResource(R.string.no_plugins_installed)) Text(stringResource(R.string.plugins_none_installed))
} }
} }
} }

View file

@ -42,31 +42,31 @@ fun SettingsScreen(
) )
} }
SettingsHeader(stringResource(R.string.appearance)) SettingsHeader(stringResource(R.string.settings_appearance))
SettingsItem( SettingsItem(
modifier = Modifier.clickable(onClick = viewModel::showThemeDialog), modifier = Modifier.clickable(onClick = viewModel::showThemeDialog),
icon = { Icon(Icons.Default.Style, null) }, icon = { Icon(Icons.Default.Style, null) },
text = { Text(stringResource(R.string.theme)) } text = { Text(stringResource(R.string.settings_theme)) }
) { ) {
FilledTonalButton(onClick = viewModel::showThemeDialog) { FilledTonalButton(onClick = viewModel::showThemeDialog) {
Text(preferences.theme.displayName) Text(preferences.theme.toDisplayName())
} }
} }
SettingsSwitch( SettingsSwitch(
label = stringResource(R.string.dynamic_color), label = stringResource(R.string.setting_dynamic_color),
pref = preferences.dynamicColor, pref = preferences.dynamicColor,
icon = { Icon(Icons.Default.Palette, null) } icon = { Icon(Icons.Default.Palette, null) }
) { ) {
preferences.dynamicColor = it preferences.dynamicColor = it
} }
SettingsHeader(stringResource(R.string.advanced)) SettingsHeader(stringResource(R.string.settings_advanced))
SettingsSwitch( SettingsSwitch(
label = stringResource(R.string.replace_bg), label = stringResource(R.string.setting_replace_icon),
secondaryLabel = stringResource(R.string.replace_bg_description), secondaryLabel = stringResource(R.string.setting_replace_icon_desc),
pref = preferences.replaceIcon, pref = preferences.replaceIcon,
icon = { Icon(Icons.Default.AppShortcut, null) } icon = { Icon(Icons.Default.AppShortcut, null) }
) { ) {
@ -76,13 +76,13 @@ fun SettingsScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SettingsTextField( SettingsTextField(
label = stringResource(R.string.app_name_setting), label = stringResource(R.string.setting_app_name),
pref = preferences.appName, pref = preferences.appName,
onPrefChange = viewModel::setAppName onPrefChange = viewModel::setAppName
) )
SettingsSwitch( SettingsSwitch(
label = stringResource(R.string.developer_options), label = stringResource(R.string.settings_developer_options),
pref = preferences.devMode, pref = preferences.devMode,
icon = { Icon(Icons.Default.Code, null) } icon = { Icon(Icons.Default.Code, null) }
) { ) {
@ -98,20 +98,20 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
SettingsTextField( SettingsTextField(
label = stringResource(R.string.package_name), label = stringResource(R.string.setting_package_name),
pref = preferences.packageName, pref = preferences.packageName,
onPrefChange = viewModel::setPackageName onPrefChange = viewModel::setPackageName
) )
SettingsTextField( SettingsTextField(
label = stringResource(R.string.version), label = stringResource(R.string.setting_target_version),
pref = preferences.version, pref = preferences.version,
onPrefChange = viewModel::setVersion onPrefChange = viewModel::setVersion
) )
SettingsSwitch( SettingsSwitch(
label = stringResource(R.string.debuggable), label = stringResource(R.string.setting_debuggable),
secondaryLabel = stringResource(R.string.debuggable_description), secondaryLabel = stringResource(R.string.setting_debuggable_desc),
pref = preferences.debuggable, pref = preferences.debuggable,
icon = { Icon(Icons.Default.BugReport, null) } icon = { Icon(Icons.Default.BugReport, null) }
) { ) {
@ -128,7 +128,7 @@ fun SettingsScreen(
onClick = viewModel::clearCacheDir onClick = viewModel::clearCacheDir
) { ) {
Text( Text(
text = stringResource(R.string.clear_cache), text = stringResource(R.string.setting_clear_cache),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
@ -148,10 +148,10 @@ fun ThemeDialog(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Default.Style, imageVector = Icons.Default.Style,
contentDescription = stringResource(R.string.theme) contentDescription = stringResource(R.string.settings_theme)
) )
}, },
title = { Text(stringResource(R.string.theme)) }, title = { Text(stringResource(R.string.settings_theme)) },
text = { text = {
Column { Column {
Theme.values().forEach { theme -> Theme.values().forEach { theme ->
@ -162,7 +162,7 @@ fun ThemeDialog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = theme.displayName, text = theme.toDisplayName(),
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge
) )
@ -183,7 +183,7 @@ fun ThemeDialog(
onDismissRequest() onDismissRequest()
} }
) { ) {
Text(stringResource(R.string.apply)) Text(stringResource(R.string.action_apply))
} }
} }
) )

View file

@ -11,6 +11,8 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@Composable @Composable
@ -42,8 +44,15 @@ fun ManagerTheme(
) )
} }
enum class Theme(val displayName: String) { enum class Theme {
SYSTEM("System"), SYSTEM,
LIGHT("Light"), LIGHT,
DARK("Dark") DARK;
@Composable
fun toDisplayName() = when (this) {
SYSTEM -> stringResource(R.string.theme_system)
LIGHT -> stringResource(R.string.theme_light)
DARK -> stringResource(R.string.theme_dark)
}
} }

View file

@ -1,6 +1,9 @@
package com.aliucord.manager.ui.util package com.aliucord.manager.ui.util
import androidx.compose.ui.text.AnnotatedString import androidx.annotation.StringRes
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
context(AnnotatedString.Builder) context(AnnotatedString.Builder)
inline fun <T> Iterable<T>.joinToAnnotatedString( inline fun <T> Iterable<T>.joinToAnnotatedString(
@ -21,3 +24,55 @@ context(AnnotatedString.Builder)
} }
postfix(this@Builder) postfix(this@Builder)
} }
/**
* This formats and styles string resources into an [AnnotatedString].
* By putting `§annotation_name§target string blah blah blah§` into the resource string it can be translated
* (assuming `annotation_name` is not changed between translations),
* when rendering [annotationHandler] will be called on each `annotation_name` and expected to return a target style on it.
*/
@Suppress("NOTHING_TO_INLINE")
@Composable
inline fun annotatingStringResource(
@StringRes id: Int,
vararg args: Any,
crossinline annotationHandler: @Composable (annotationName: String) -> SpanStyle?
): AnnotatedString {
val string = stringResource(id, *args)
// TODO: figure out a way to put this in remember {} while still having @Composable annotationHandler
val markerIndexes = string
.mapIndexedNotNull { index, elem -> index.takeIf { elem == '§' } }
.takeIf { it.isNotEmpty() && it.size % 3 == 0 }
?: return AnnotatedString(string)
return buildAnnotatedString {
var lastIndex = 0
var offset = 0
for ((nameStart, nameEnd, valueEnd) in markerIndexes.chunked(3)) {
append(string.substring(lastIndex, nameStart))
append(string.substring(nameEnd + 1, valueEnd))
val annotationName = string.substring(nameStart + 1, nameEnd)
val newValueStart = nameEnd - annotationName.length - offset - 1
val newValueEnd = valueEnd - annotationName.length - offset - 2
addStringAnnotation(
tag = annotationName,
annotation = annotationName,
start = newValueStart,
end = newValueEnd,
)
annotationHandler(annotationName)?.let {
addStyle(it, newValueStart, newValueEnd)
}
lastIndex = valueEnd + 1
offset += 3 + annotationName.length
}
append(string.substring(lastIndex))
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.* import androidx.paging.*
import com.aliucord.manager.R
import com.aliucord.manager.domain.manager.PreferencesManager import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.network.dto.Commit import com.aliucord.manager.network.dto.Commit
@ -25,7 +26,10 @@ class HomeViewModel(
var supportedVersion by mutableStateOf("") var supportedVersion by mutableStateOf("")
private set private set
var installedVersion by mutableStateOf("-") var supportedVersionType by mutableStateOf(R.string.version_unknown)
private set
var installedVersion by mutableStateOf("")
private set private set
val commits = Pager(PagingConfig(pageSize = 30)) { val commits = Pager(PagingConfig(pageSize = 30)) {
@ -62,6 +66,7 @@ class HomeViewModel(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
packageManager.getPackageInfo(preferences.packageName, 0).versionName packageManager.getPackageInfo(preferences.packageName, 0).versionName
} catch (th: Throwable) { } catch (th: Throwable) {
// TODO: translated error message
"-" "-"
} }
} }
@ -70,16 +75,19 @@ class HomeViewModel(
private suspend fun _fetchSupportedVersion() { private suspend fun _fetchSupportedVersion() {
val version = githubRepository.getVersion() val version = githubRepository.getVersion()
supportedVersion = version.fold( version.fold(
success = { success = {
"${it.versionName} - " + when (it.versionCode[3]) { supportedVersion = it.versionName
'0' -> "Stable" supportedVersionType = when (it.versionCode[3]) {
'1' -> "Beta" '0' -> R.string.version_stable
'2' -> "Alpha" '1' -> R.string.version_beta
else -> throw NoWhenBranchMatchedException() '2' -> R.string.version_alpha
else -> R.string.version_unknown
} }
}, },
fail = { "Failed to retrieve version" } fail = {
supportedVersionType = R.string.version_load_fail
}
) )
} }
@ -95,7 +103,7 @@ class HomeViewModel(
if (launchIntent != null) { if (launchIntent != null) {
application.startActivity(launchIntent) application.startActivity(launchIntent)
} else { } else {
Toast.makeText(application, "Failed to launch Aliucord", Toast.LENGTH_LONG).show() Toast.makeText(application, R.string.launch_aliucord_fail, Toast.LENGTH_LONG).show()
} }
} }

View file

@ -1,59 +1,88 @@
<resources> <resources>
<string name="app_name">Aliucord Manager</string> <string name="app_name">Aliucord Manager</string>
<string name="clear_cache">Clear cache</string> <string name="app_description">A modification for the Discord Android App</string>
<string name="commits">Commits</string>
<string name="dex_location">Dex location</string> <string name="aliucord">Aliucord</string>
<string name="open_settings">Open settings</string>
<string name="replace_bg">Replace app icon</string>
<string name="replace_bg_description">Uses the Aliucord variant as opposed to Discord\'s own icon</string>
<string name="settings">Settings</string>
<string name="show_menu">Show menu</string>
<string name="theme">Theme</string>
<string name="use_dex_from_storage">Use Injector.dex from storage</string>
<string name="plugins">Plugins</string>
<string name="search">Search</string>
<string name="view_plugin_changelog">View changelog for %1$s</string>
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
<string name="close">Close</string>
<string name="support_server">Support Server</string>
<string name="back">Back</string>
<string name="home">Home</string>
<string name="install">Install</string>
<string name="reinstall">Reinstall</string>
<string name="update">Update</string>
<string name="developer_options">Developer options</string>
<string name="debuggable">Debuggable</string>
<string name="debuggable_description">Enable debuggable flag</string>
<string name="about">About</string>
<string name="team">Team</string>
<string name="contributors">Contributors</string>
<string name="search_plugins">Search Plugins</string>
<string name="github">GitHub</string> <string name="github">GitHub</string>
<string name="support_server">Support Server</string>
<string name="commits">Commits</string>
<string name="team">Team</string>
<string name="website">Website</string> <string name="website">Website</string>
<string name="installer">Installer</string> <string name="installer">Installer</string>
<string name="contributions">%o contributions</string>
<string name="theme_setting_description">Manager appearance</string> <string name="action_retry">Retry</string>
<string name="no_plugins_installed">No plugins installed!</string> <string name="action_apply">Apply</string>
<string name="contributor">Contributor</string> <string name="action_confirm">Confirm</string>
<string name="apply">Apply</string> <string name="action_dismiss">Dismiss</string>
<string name="confirm">Confirm</string> <string name="action_install">Install</string>
<string name="dismiss">Dismiss</string> <string name="action_reinstall">Reinstall</string>
<string name="download_apk">Download APK</string> <string name="action_update">Update</string>
<string name="select_apk">Select APK</string> <string name="action_clear">Clear</string>
<string name="download_method">Download Method</string> <string name="action_launch">Launch</string>
<string name="changelog_media">Changelog Media</string> <string name="action_uninstall">Uninstall</string>
<string name="app_name_setting">App Name</string> <string name="action_close">Close</string>
<string name="package_name">Package Name</string> <string name="action_open_settings">Open settings</string>
<string name="clear">Clear</string> <string name="action_search">Search</string>
<string name="launch">Launch</string> <string name="action_loading_trailing">Loading…</string>
<string name="uninstall">Uninstall</string>
<string name="version">Download Version</string> <string name="permissions_grant_title">Grant Permissions</string>
<string name="dynamic_color">Dynamic color</string>
<string name="appearance">Appearance</string>
<string name="advanced">Advanced</string>
<string name="permissions_grant">Grant Permissions</string>
<string name="permissions_grant_body">In order for Aliucord Manager to work properly, you need to grant file permissions. Since shared data in stored in ~/Aliucord, permissions are required in order for manager to function.</string> <string name="permissions_grant_body">In order for Aliucord Manager to work properly, you need to grant file permissions. Since shared data in stored in ~/Aliucord, permissions are required in order for manager to function.</string>
<string name="aliucord_description">A modification for the Discord Android App</string>
<string name="load_fail">Failed to load</string> <string name="install_success">Successfully installed Aliucord</string>
<string name="retry">Retry</string> <string name="install_aborted">Aborted Aliucord installation</string>
<string name="install_failed">Installation failed with error %o</string>
<string name="launch_aliucord_fail">Failed to launch Aliucord</string>
<string name="settings_theme">Theme</string>
<string name="settings_advanced">Advanced</string>
<string name="settings_appearance">Appearance</string>
<string name="settings_developer_options">Developer options</string>
<string name="setting_clear_cache">Clear cache</string>
<string name="setting_dex_location">Dex location</string>
<string name="setting_use_dex_from_storage">Use Injector.dex from storage</string>
<string name="setting_replace_icon">Replace app icon</string>
<string name="setting_replace_icon_desc">Uses the Aliucord variant as opposed to Discord\'s own icon</string>
<string name="setting_debuggable">Debuggable</string>
<string name="setting_debuggable_desc">Enable debuggable flag</string>
<string name="setting_theme_desc">Manager appearance</string>
<string name="setting_app_name">App Name</string>
<string name="setting_package_name">Package Name</string>
<string name="setting_target_version">Download Version</string>
<string name="setting_dynamic_color">Dynamic color</string>
<string name="theme_system">System</string>
<string name="theme_dark">Dark</string>
<string name="theme_light">Light</string>
<string name="plugins_title">Plugins</string>
<string name="plugins_view_changelog">View changelog for %1$s</string>
<string name="plugins_delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
<string name="plugins_search">Search Plugins</string>
<string name="plugins_none_installed">No plugins installed!</string>
<string name="plugins_changelog_media">Changelog Media</string>
<string name="plugins_plugin_title">§plugin§%1$s§ v%2$s</string>
<string name="navigation_back">Back</string>
<string name="navigation_home">Home</string>
<string name="navigation_about">About</string>
<string name="navigation_settings">Settings</string>
<string name="contributors">Contributors</string>
<string name="contributors_contributions">%o contributions</string>
<string name="contributors_contributor">Contributor</string>
<string name="contributors_commit_title">§name§%1$s§ §middle§authored§ §commit§%2$s§</string>
<string name="selector_download_apk">Download APK</string>
<string name="selector_select_apk">Select APK</string>
<string name="selector_download_method">Download Method</string>
<string name="network_load_fail">Failed to load</string>
<string name="version_supported">Supported version:</string>
<string name="version_installed">Installed version:</string>
<string name="version_stable">Stable</string>
<string name="version_beta">Beta</string>
<string name="version_alpha">Alpha</string>
<string name="version_unknown" translatable="false" />
<string name="version_load_fail">Failed to retrieve version</string>
</resources> </resources>

7
crowdin.yml Normal file
View file

@ -0,0 +1,7 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
files:
- source: app/src/main/res/values/strings.xml
translation: app/src/main/res/values-%android_code%/strings.xml