Merge pull request #1824 from vitorpamplona/claude/improve-translation-settings-dDwmL

Redesign language settings UI with searchable language pickers
This commit is contained in:
Vitor Pamplona
2026-03-13 08:16:58 -04:00
committed by GitHub
2 changed files with 446 additions and 168 deletions
@@ -20,28 +20,43 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
import android.content.res.Resources import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.IconButton
import androidx.compose.material3.InputChip
import androidx.compose.material3.InputChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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
@@ -49,10 +64,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -64,7 +78,6 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import java.util.Locale as JavaLocale import java.util.Locale as JavaLocale
@@ -97,141 +110,219 @@ fun UserSettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = SpacedBy10dp, verticalArrangement = SpacedBy10dp,
) { ) {
DontTranslateFromSetting(accountViewModel)
TranslateToSetting(accountViewModel) TranslateToSetting(accountViewModel)
HorizontalDivider(thickness = DividerThickness)
DontTranslateFromSetting(accountViewModel)
HorizontalDivider(thickness = DividerThickness)
LanguagePreferencesSetting(accountViewModel) LanguagePreferencesSetting(accountViewModel)
} }
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class) private fun getAllLanguagesSorted(): List<JavaLocale> {
val seen = mutableSetOf<String>()
return JavaLocale.getAvailableLocales()
.filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) }
.sortedBy { it.displayName.lowercase() }
}
@Composable @Composable
fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { private fun SearchableLanguageList(
var expanded by remember { mutableStateOf(false) } languages: List<JavaLocale>,
val selectedLanguages = accountViewModel.dontTranslateFromFilteredBySpokenLanguages().toMutableSet() onSelect: (JavaLocale) -> Unit,
modifier: Modifier = Modifier,
) {
var searchQuery by remember { mutableStateOf("") }
val filtered =
remember(searchQuery, languages) {
if (searchQuery.isBlank()) {
languages
} else {
languages.filter {
it.displayName.contains(searchQuery, ignoreCase = true) ||
it.language.contains(searchQuery, ignoreCase = true)
}
}
}
Column { Column(modifier) {
SettingsRow( OutlinedTextField(
name = R.string.dont_translate_from, value = searchQuery,
description = R.string.dont_translate_from_description, onValueChange = { searchQuery = it },
) { placeholder = { Text(stringRes(R.string.search_languages)) },
ExposedDropdownMenuBox( leadingIcon = {
expanded = expanded, Icon(
onExpandedChange = { expanded = !expanded }, imageVector = Icons.Default.Search,
) { contentDescription = null,
OutlinedTextField( modifier = Modifier.size(20.dp),
value = stringRes(R.string.quick_action_select),
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
) )
},
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
ExposedDropdownMenu( Spacer(modifier = Modifier.height(4.dp))
expanded = expanded,
onDismissRequest = { expanded = false }, LazyColumn(modifier = Modifier.heightIn(max = 200.dp)) {
items(filtered, key = { it.language }) { locale ->
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable { onSelect(locale) }
.padding(horizontal = 8.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
selectedLanguages.forEach { languageCode -> Text(
DropdownMenuItem( text = locale.displayName,
text = { Text(text = JavaLocale.forLanguageTag(languageCode).displayName) }, style = MaterialTheme.typography.bodyMedium,
onClick = { modifier = Modifier.weight(1f),
accountViewModel.toggleDontTranslateFrom(languageCode) )
selectedLanguages.remove(languageCode) Text(
expanded = false text = locale.language,
}, style = MaterialTheme.typography.bodySmall,
trailingIcon = { color = MaterialTheme.colorScheme.onSurfaceVariant,
Icon( )
imageVector = Icons.Default.Close,
contentDescription = stringRes(R.string.remove_language, languageCode),
tint = Color.Red,
modifier = Modifier.size(16.dp),
)
},
)
}
} }
} }
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TranslateToSetting(accountViewModel: AccountViewModel) { fun TranslateToSetting(accountViewModel: AccountViewModel) {
var expanded by remember { mutableStateOf(false) }
val currentTranslateTo = accountViewModel.translateTo() val currentTranslateTo = accountViewModel.translateTo()
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().configuration) val allLanguages = remember { getAllLanguagesSorted() }
var showPicker by remember { mutableStateOf(false) }
Column { Column(modifier = Modifier.fillMaxWidth()) {
SettingsRow( SettingsRow(
name = R.string.translate_to, name = R.string.translate_to,
description = R.string.translate_to_description, description = R.string.translate_to_description,
) { ) {
ExposedDropdownMenuBox( OutlinedCard(
expanded = expanded, modifier = Modifier.clickable { showPicker = !showPicker },
onExpandedChange = { expanded = !expanded },
) { ) {
OutlinedTextField( Row(
value = JavaLocale(currentTranslateTo).displayName, modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
onValueChange = {}, verticalAlignment = Alignment.CenterVertically,
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) { ) {
for (i in 0 until languageList.size()) { Text(
languageList.get(i)?.let { lang -> text = JavaLocale(currentTranslateTo).displayName,
DropdownMenuItem( style = MaterialTheme.typography.bodyMedium,
text = { fontWeight = FontWeight.Medium,
Row(verticalAlignment = Alignment.CenterVertically) { )
if (accountViewModel.account.settings.translateToContains(lang)) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(24.dp),
)
} else {
Spacer(modifier = Modifier.size(24.dp))
}
Spacer(modifier = Modifier.size(10.dp))
Text(text = lang.displayName)
}
},
onClick = {
accountViewModel.updateTranslateTo(lang)
expanded = false
},
)
}
}
} }
} }
} }
if (showPicker) {
Spacer(modifier = Modifier.height(8.dp))
SearchableLanguageList(
languages = allLanguages,
onSelect = { locale ->
accountViewModel.updateTranslateTo(locale)
showPicker = false
},
)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DontTranslateFromSetting(accountViewModel: AccountViewModel) {
val selectedLanguages = accountViewModel.dontTranslateFrom().toSet()
var showAddPicker by remember { mutableStateOf(false) }
val allLanguages = remember { getAllLanguagesSorted() }
val availableToAdd =
remember(selectedLanguages, allLanguages) {
allLanguages.filter { it.language !in selectedLanguages }
}
Column(modifier = Modifier.fillMaxWidth()) {
SettingsRow(
name = R.string.dont_translate_from,
description = R.string.dont_translate_from_description,
) {}
Spacer(modifier = Modifier.height(8.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth(),
) {
selectedLanguages.forEach { languageCode ->
InputChip(
selected = true,
onClick = { accountViewModel.toggleDontTranslateFrom(languageCode) },
label = { Text(JavaLocale(languageCode).displayName) },
trailingIcon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringRes(R.string.remove_language, languageCode),
modifier = Modifier.size(InputChipDefaults.IconSize),
)
},
)
}
InputChip(
selected = false,
onClick = { showAddPicker = !showAddPicker },
label = { Text(stringRes(R.string.add_language)) },
leadingIcon = {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(InputChipDefaults.IconSize),
)
},
)
}
if (showAddPicker) {
Spacer(modifier = Modifier.height(8.dp))
SearchableLanguageList(
languages = availableToAdd,
onSelect = { locale ->
accountViewModel.toggleDontTranslateFrom(locale.language)
showAddPicker = false
},
)
}
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) {
val languagePreferences = accountViewModel.account.settings.syncedSettings.languages.languagePreferences val languagePreferences = accountViewModel.account.settings.syncedSettings.languages.languagePreferences
var showAddPair by remember { mutableStateOf(false) }
if (languagePreferences.isEmpty()) return Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(verticalArrangement = SpacedBy5dp) {
SettingsRow( SettingsRow(
name = R.string.language_preferences, name = R.string.language_preferences,
description = R.string.language_preferences_description, description = R.string.language_preferences_description,
) {} ) {}
if (languagePreferences.isEmpty() && !showAddPair) {
Text(
text = stringRes(R.string.no_language_preferences),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 8.dp),
)
}
languagePreferences.forEach { (key, preference) -> languagePreferences.forEach { (key, preference) ->
val parts = key.split(",") val parts = key.split(",")
if (parts.size == 2) { if (parts.size == 2) {
LanguagePreferenceItem( LanguagePreferenceCard(
source = parts[0], source = parts[0],
target = parts[1], target = parts[1],
preference = preference, preference = preference,
@@ -239,12 +330,31 @@ fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) {
) )
} }
} }
TextButton(
onClick = { showAddPair = !showAddPair },
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Text(stringRes(R.string.add_language_pair))
}
if (showAddPair) {
AddLanguagePairCard(
accountViewModel = accountViewModel,
onDismiss = { showAddPair = false },
)
}
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun LanguagePreferenceItem( private fun LanguagePreferenceCard(
source: String, source: String,
target: String, target: String,
preference: String, preference: String,
@@ -252,77 +362,237 @@ private fun LanguagePreferenceItem(
) { ) {
val sourceName = JavaLocale(source).displayName val sourceName = JavaLocale(source).displayName
val targetName = JavaLocale(target).displayName val targetName = JavaLocale(target).displayName
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox( OutlinedCard(modifier = Modifier.fillMaxWidth()) {
expanded = expanded, Column(modifier = Modifier.padding(12.dp)) {
onExpandedChange = { expanded = !expanded }, Row(
modifier = Modifier.padding(vertical = 4.dp), modifier = Modifier.fillMaxWidth(),
) { verticalAlignment = Alignment.CenterVertically,
OutlinedTextField( ) {
value = stringRes(R.string.language_preference_pair, sourceName, targetName), Icon(
onValueChange = {}, imageVector = Icons.Default.SwapHoriz,
readOnly = true, contentDescription = null,
label = { tint = MaterialTheme.colorScheme.primary,
Text( modifier = Modifier.size(20.dp),
if (preference == source) {
stringRes(R.string.translations_show_in_lang_first, sourceName)
} else {
stringRes(R.string.translations_show_in_lang_first, targetName)
},
) )
}, Spacer(modifier = Modifier.width(8.dp))
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, Text(
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), text = stringRes(R.string.language_preference_pair, sourceName, targetName),
) style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
)
IconButton(
onClick = {
accountViewModel.prefer(source, target, preference)
},
modifier = Modifier.size(32.dp),
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringRes(R.string.delete_preference),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(18.dp),
)
}
}
ExposedDropdownMenu( Spacer(modifier = Modifier.height(8.dp))
expanded = expanded,
onDismissRequest = { expanded = false }, Row(
) { modifier =
DropdownMenuItem( Modifier
text = { .fillMaxWidth()
Row(verticalAlignment = Alignment.CenterVertically) { .clickable { accountViewModel.prefer(source, target, source) }
if (preference == source) { .padding(vertical = 4.dp),
Icon( verticalAlignment = Alignment.CenterVertically,
imageVector = Icons.Default.Check, ) {
contentDescription = null, RadioButton(
modifier = Modifier.size(24.dp), selected = preference == source,
) onClick = { accountViewModel.prefer(source, target, source) },
} else { )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.width(4.dp))
} Text(
Spacer(modifier = Modifier.size(10.dp)) text = stringRes(R.string.show_first, sourceName),
Text(stringRes(R.string.translations_show_in_lang_first, sourceName)) style = MaterialTheme.typography.bodyMedium,
} )
}, }
onClick = {
accountViewModel.prefer(source, target, source) Row(
expanded = false modifier =
}, Modifier
) .fillMaxWidth()
HorizontalDivider(thickness = DividerThickness) .clickable { accountViewModel.prefer(source, target, target) }
DropdownMenuItem( .padding(vertical = 4.dp),
text = { verticalAlignment = Alignment.CenterVertically,
Row(verticalAlignment = Alignment.CenterVertically) { ) {
if (preference == target) { RadioButton(
Icon( selected = preference == target,
imageVector = Icons.Default.Check, onClick = { accountViewModel.prefer(source, target, target) },
contentDescription = null, )
modifier = Modifier.size(24.dp), Spacer(modifier = Modifier.width(4.dp))
) Text(
} else { text = stringRes(R.string.show_first, targetName),
Spacer(modifier = Modifier.size(24.dp)) style = MaterialTheme.typography.bodyMedium,
} )
Spacer(modifier = Modifier.size(10.dp)) }
Text(stringRes(R.string.translations_show_in_lang_first, targetName)) }
} }
}, }
onClick = {
accountViewModel.prefer(source, target, target) @Composable
expanded = false private fun AddLanguagePairCard(
}, accountViewModel: AccountViewModel,
) onDismiss: () -> Unit,
) {
val allLanguages = remember { getAllLanguagesSorted() }
var selectedSource by remember { mutableStateOf<JavaLocale?>(null) }
var selectedTarget by remember { mutableStateOf<JavaLocale?>(null) }
var pickingSource by remember { mutableStateOf(true) }
var pickingTarget by remember { mutableStateOf(false) }
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = stringRes(R.string.add_language_pair),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedCard(
modifier =
Modifier
.weight(1f)
.clickable {
pickingSource = true
pickingTarget = false
},
) {
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = stringRes(R.string.source_language),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = selectedSource?.displayName ?: stringRes(R.string.quick_action_select),
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (selectedSource != null) FontWeight.Medium else FontWeight.Normal,
color =
if (selectedSource != null) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
)
}
}
Icon(
imageVector = Icons.Default.SwapHoriz,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp),
)
OutlinedCard(
modifier =
Modifier
.weight(1f)
.clickable {
pickingTarget = true
pickingSource = false
},
) {
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = stringRes(R.string.target_language),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = selectedTarget?.displayName ?: stringRes(R.string.quick_action_select),
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (selectedTarget != null) FontWeight.Medium else FontWeight.Normal,
color =
if (selectedTarget != null) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
)
}
}
}
if (pickingSource) {
Spacer(modifier = Modifier.height(8.dp))
SearchableLanguageList(
languages = allLanguages,
onSelect = { locale ->
selectedSource = locale
pickingSource = false
if (selectedTarget == null) {
pickingTarget = true
}
},
)
}
if (pickingTarget) {
Spacer(modifier = Modifier.height(8.dp))
SearchableLanguageList(
languages = allLanguages.filter { it.language != selectedSource?.language },
onSelect = { locale ->
selectedTarget = locale
pickingTarget = false
},
)
}
if (selectedSource != null && selectedTarget != null) {
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(onClick = onDismiss) {
Text(stringRes(R.string.go_back))
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(
onClick = {
val src = selectedSource!!.language
val tgt = selectedTarget!!.language
accountViewModel.prefer(src, tgt, tgt)
onDismiss()
},
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Text(
stringRes(
R.string.show_first,
selectedTarget!!.displayName,
),
)
}
}
}
} }
} }
} }
+8
View File
@@ -1508,6 +1508,14 @@
<string name="language_preferences">Language Display Preferences</string> <string name="language_preferences">Language Display Preferences</string>
<string name="language_preferences_description">For each translated language pair, choose which language to show first.</string> <string name="language_preferences_description">For each translated language pair, choose which language to show first.</string>
<string name="language_preference_pair">%1$s → %2$s</string> <string name="language_preference_pair">%1$s → %2$s</string>
<string name="search_languages">Search languages</string>
<string name="add_language">Add language</string>
<string name="add_language_pair">Add language pair</string>
<string name="source_language">Source language</string>
<string name="target_language">Target language</string>
<string name="show_first">Show %1$s first</string>
<string name="no_language_preferences">No language display preferences yet. These are created automatically when translations occur, or you can add them manually.</string>
<string name="delete_preference">Delete preference</string>
<string name="pause">Pause</string> <string name="pause">Pause</string>
<string name="play">Play</string> <string name="play">Play</string>
<string name="open_dropdown_menu">Open dropdown menu</string> <string name="open_dropdown_menu">Open dropdown menu</string>