implement language direction and shuffling logic in StartExerciseScreen

This commit is contained in:
jonasgaudian
2026-02-17 13:55:15 +01:00
parent a0b6509367
commit d14940ed11
5 changed files with 134 additions and 26 deletions

View File

@@ -56,6 +56,8 @@ fun BaseLanguageDropDown(
enableMultipleSelection: Boolean = false, enableMultipleSelection: Boolean = false,
onLanguagesSelected: (List<Language>) -> Unit = {}, onLanguagesSelected: (List<Language>) -> Unit = {},
alternateLanguages: List<Language> = emptyList(), alternateLanguages: List<Language> = emptyList(),
restrictToAlternateLanguages: Boolean = false,
enabled: Boolean = true,
iconEnabled: Boolean = true, iconEnabled: Boolean = true,
noBorder: Boolean = false, noBorder: Boolean = false,
) { ) {
@@ -68,9 +70,13 @@ fun BaseLanguageDropDown(
var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) } var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) }
var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) } var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) }
val languages = remember(alternateLanguages, defaultLanguages) { val languages = remember(alternateLanguages, defaultLanguages, restrictToAlternateLanguages) {
if (restrictToAlternateLanguages) {
alternateLanguages
} else {
alternateLanguages.ifEmpty { defaultLanguages } alternateLanguages.ifEmpty { defaultLanguages }
} }
}
val buttonText = when { val buttonText = when {
enableMultipleSelection && selectedLanguagesCount > 0 -> stringResource( enableMultipleSelection && selectedLanguagesCount > 0 -> stringResource(
@@ -90,6 +96,7 @@ fun BaseLanguageDropDown(
AppOutlinedButton( AppOutlinedButton(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
onClick = { expanded = true }, onClick = { expanded = true },
enabled = enabled,
contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp), contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp),
borderColor = if (noBorder) Color.Unspecified else null borderColor = if (noBorder) Color.Unspecified else null
) { ) {
@@ -222,7 +229,12 @@ fun BaseLanguageDropDown(
val isSearching = searchText.isNotBlank() val isSearching = searchText.isNotBlank()
if (isSearching) { if (isSearching) {
val searchResults = (favoriteLanguages + languageHistory + languages) val searchBase = if (restrictToAlternateLanguages) {
alternateLanguages
} else {
favoriteLanguages + languageHistory + languages
}
val searchResults = searchBase
.distinctBy { it.nameResId } .distinctBy { it.nameResId }
.filter { language -> .filter { language ->
val matchesName = language.name.contains(searchText, ignoreCase = true) val matchesName = language.name.contains(searchText, ignoreCase = true)
@@ -237,6 +249,16 @@ fun BaseLanguageDropDown(
searchResults.forEach { language -> SingleSelectItem(language) } searchResults.forEach { language -> SingleSelectItem(language) }
} }
} else if (restrictToAlternateLanguages) {
val sortedAlternate = alternateLanguages.sortedBy { it.name }
if (enableMultipleSelection) {
DropdownHeader(text = stringResource(R.string.text_all_languages))
sortedAlternate.forEach { language -> MultiSelectItem(language) }
} else {
DropdownHeader(text = stringResource(R.string.text_all_languages))
sortedAlternate.forEach { language -> SingleSelectItem(language) }
}
} else if (alternateLanguages.isNotEmpty()) { } else if (alternateLanguages.isNotEmpty()) {
val sortedAlternate = alternateLanguages.sortedBy { it.name } val sortedAlternate = alternateLanguages.sortedBy { it.name }
if (enableMultipleSelection) { if (enableMultipleSelection) {
@@ -458,7 +480,9 @@ fun SingleLanguageDropDown(
onAutoSelected: () -> Unit = {}, onAutoSelected: () -> Unit = {},
showNoneOption: Boolean = false, showNoneOption: Boolean = false,
onNoneSelected: () -> Unit = {}, onNoneSelected: () -> Unit = {},
alternateLanguages: List<Language> = emptyList() alternateLanguages: List<Language> = emptyList(),
restrictToAlternateLanguages: Boolean = false,
enabled: Boolean = true
) { ) {
val languageHistory by languageViewModel.languageHistory.collectAsState() val languageHistory by languageViewModel.languageHistory.collectAsState()
@@ -477,6 +501,10 @@ fun SingleLanguageDropDown(
showNoneOption = showNoneOption, showNoneOption = showNoneOption,
onNoneSelected = onNoneSelected, onNoneSelected = onNoneSelected,
enableMultipleSelection = false, enableMultipleSelection = false,
alternateLanguages = alternateLanguages alternateLanguages = alternateLanguages,
restrictToAlternateLanguages = restrictToAlternateLanguages,
enabled = enabled,
iconEnabled = enabled,
noBorder = !enabled
) )
} }

View File

@@ -89,6 +89,7 @@ fun StartExerciseScreen(
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) } var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) } var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
val isDirectionPreferenceSet = selectedOriginLanguage != null || selectedTargetLanguage != null
val selectedPairsIds = remember(selectedLanguagePairs) { val selectedPairsIds = remember(selectedLanguagePairs) {
selectedLanguagePairs.map { it.first.nameResId to it.second.nameResId } selectedLanguagePairs.map { it.first.nameResId to it.second.nameResId }
@@ -115,6 +116,15 @@ fun StartExerciseScreen(
val itemsToShow by filteredItemsFlow.collectAsState(initial = emptyList()) val itemsToShow by filteredItemsFlow.collectAsState(initial = emptyList())
val totalItemCount = itemsToShow.size val totalItemCount = itemsToShow.size
val availableLanguagesFromItems = remember(itemsToShow, selectedPairsIds) {
val ids = if (selectedPairsIds.isNotEmpty()) {
selectedPairsIds.flatMap { pair -> listOf(pair.first, pair.second) }.toSet()
} else {
itemsToShow.flatMap { listOfNotNull(it.languageFirstId, it.languageSecondId) }.toSet()
}
ids
}
var amount by remember { mutableStateOf(0) } var amount by remember { mutableStateOf(0) }
androidx.compose.runtime.LaunchedEffect(totalItemCount) { androidx.compose.runtime.LaunchedEffect(totalItemCount) {
amount = totalItemCount amount = totalItemCount
@@ -140,6 +150,7 @@ fun StartExerciseScreen(
onShuffleCardsChanged = { updateConfig(exerciseConfig.copy(shuffleCards = it)) }, onShuffleCardsChanged = { updateConfig(exerciseConfig.copy(shuffleCards = it)) },
shuffleLanguages = exerciseConfig.shuffleLanguages, shuffleLanguages = exerciseConfig.shuffleLanguages,
onShuffleLanguagesChanged = { updateConfig(exerciseConfig.copy(shuffleLanguages = it)) }, onShuffleLanguagesChanged = { updateConfig(exerciseConfig.copy(shuffleLanguages = it)) },
shuffleLanguagesEnabled = !isDirectionPreferenceSet,
trainingMode = exerciseConfig.trainingMode, trainingMode = exerciseConfig.trainingMode,
onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) }, onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) },
) )
@@ -154,21 +165,46 @@ fun StartExerciseScreen(
item { item {
LanguagePairSection( LanguagePairSection(
selectedPairs = selectedLanguagePairs, selectedPairs = selectedLanguagePairs,
onPairsChanged = { selectedLanguagePairs = it }, availableLanguageIds = availableLanguagesFromItems,
onPairsChanged = { updatedPairs ->
val hadPairs = selectedLanguagePairs.isNotEmpty()
selectedLanguagePairs = updatedPairs
if (updatedPairs.isNotEmpty()) {
selectedOriginLanguage = null
selectedTargetLanguage = null
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
} else if (hadPairs) {
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
}
},
onOriginLanguageSelected = { language -> onOriginLanguageSelected = { language ->
if (language?.nameResId == selectedOriginLanguage?.nameResId) {
selectedOriginLanguage = null
updateConfig(exerciseConfig.copy(originLanguageId = null))
} else {
selectedOriginLanguage = language selectedOriginLanguage = language
if (selectedTargetLanguage?.nameResId == language?.nameResId) { if (selectedTargetLanguage?.nameResId == language?.nameResId) {
selectedTargetLanguage = null selectedTargetLanguage = null
updateConfig(exerciseConfig.copy(targetLanguageId = null))
} }
updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId)) updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId))
}
}, },
onTargetLanguageSelected = { language -> onTargetLanguageSelected = { language ->
if (language?.nameResId == selectedTargetLanguage?.nameResId) {
selectedTargetLanguage = null
updateConfig(exerciseConfig.copy(targetLanguageId = null))
} else {
selectedTargetLanguage = language selectedTargetLanguage = language
if (selectedOriginLanguage?.nameResId == language?.nameResId) { if (selectedOriginLanguage?.nameResId == language?.nameResId) {
selectedOriginLanguage = null selectedOriginLanguage = null
updateConfig(exerciseConfig.copy(originLanguageId = null))
} }
updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId)) updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId))
}
}, },
languageSelectionEnabled = true,
selectedPairsCount = selectedLanguagePairs.size,
selectedOriginLanguage = selectedOriginLanguage, selectedOriginLanguage = selectedOriginLanguage,
selectedTargetLanguage = selectedTargetLanguage selectedTargetLanguage = selectedTargetLanguage
) )
@@ -249,6 +285,7 @@ fun TopBarSection(
onShuffleCardsChanged: (Boolean) -> Unit, onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean, shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit, onShuffleLanguagesChanged: (Boolean) -> Unit,
shuffleLanguagesEnabled: Boolean,
trainingMode: Boolean, trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit onTrainingModeChanged: (Boolean) -> Unit
) { ) {
@@ -306,6 +343,7 @@ fun TopBarSection(
onShuffleCardsChanged = onShuffleCardsChanged, onShuffleCardsChanged = onShuffleCardsChanged,
shuffleLanguages = shuffleLanguages, shuffleLanguages = shuffleLanguages,
onShuffleLanguagesChanged = onShuffleLanguagesChanged, onShuffleLanguagesChanged = onShuffleLanguagesChanged,
shuffleLanguagesEnabled = shuffleLanguagesEnabled,
trainingMode = trainingMode, trainingMode = trainingMode,
onTrainingModeChanged = onTrainingModeChanged, onTrainingModeChanged = onTrainingModeChanged,
onDismiss = { onDismiss = {
@@ -348,9 +386,12 @@ fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -
@Composable @Composable
fun LanguagePairSection( fun LanguagePairSection(
selectedPairs: List<Pair<Language, Language>>, selectedPairs: List<Pair<Language, Language>>,
availableLanguageIds: Set<Int>,
onPairsChanged: (List<Pair<Language, Language>>) -> Unit, onPairsChanged: (List<Pair<Language, Language>>) -> Unit,
onOriginLanguageSelected: (Language?) -> Unit, onOriginLanguageSelected: (Language?) -> Unit,
onTargetLanguageSelected: (Language?) -> Unit, onTargetLanguageSelected: (Language?) -> Unit,
languageSelectionEnabled: Boolean,
selectedPairsCount: Int,
selectedOriginLanguage: Language?, selectedOriginLanguage: Language?,
selectedTargetLanguage: Language? selectedTargetLanguage: Language?
) { ) {
@@ -360,9 +401,8 @@ fun LanguagePairSection(
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsState(initial = emptySet()) val languagesPresent by vocabularyViewModel.languagesPresent.collectAsState(initial = emptySet())
val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList()) val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList())
val availableLanguages = remember(languagesPresent, allLanguages) { val availableLanguages = remember(availableLanguageIds, allLanguages) {
val presentIds = languagesPresent.filterNotNull().toSet() allLanguages.filter { it.nameResId in availableLanguageIds }
allLanguages.filter { it.nameResId in presentIds }
} }
val allItems by vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()) val allItems by vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList())
@@ -433,6 +473,14 @@ fun LanguagePairSection(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
if (!languageSelectionEnabled && selectedPairsCount > 0) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.text_language_direction_disabled_with_pairs),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -448,9 +496,15 @@ fun LanguagePairSection(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
selectedLanguage = selectedOriginLanguage, selectedLanguage = selectedOriginLanguage,
onLanguageSelected = onOriginLanguageSelected, onLanguageSelected = { language ->
if (selectedTargetLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
onOriginLanguageSelected(language)
},
showNoneOption = true, showNoneOption = true,
alternateLanguages = availableLanguages onNoneSelected = { onOriginLanguageSelected(null) },
alternateLanguages = availableLanguages,
restrictToAlternateLanguages = true,
enabled = languageSelectionEnabled
) )
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@@ -463,9 +517,15 @@ fun LanguagePairSection(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
selectedLanguage = selectedTargetLanguage, selectedLanguage = selectedTargetLanguage,
onLanguageSelected = onTargetLanguageSelected, onLanguageSelected = { language ->
if (selectedOriginLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
onTargetLanguageSelected(language)
},
showNoneOption = true, showNoneOption = true,
alternateLanguages = availableLanguages onNoneSelected = { onTargetLanguageSelected(null) },
alternateLanguages = availableLanguages,
restrictToAlternateLanguages = true,
enabled = languageSelectionEnabled
) )
} }
} }
@@ -807,6 +867,7 @@ private fun StartExerciseSettingsBottomSheet(
onShuffleCardsChanged: (Boolean) -> Unit, onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean, shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit, onShuffleLanguagesChanged: (Boolean) -> Unit,
shuffleLanguagesEnabled: Boolean,
trainingMode: Boolean, trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit, onTrainingModeChanged: (Boolean) -> Unit,
onDismiss: () -> Unit onDismiss: () -> Unit
@@ -835,9 +896,22 @@ private fun StartExerciseSettingsBottomSheet(
OptionItemSwitch( OptionItemSwitch(
title = stringResource(R.string.text_shuffle_languages), title = stringResource(R.string.text_shuffle_languages),
description = stringResource(R.string.text_shuffle_languages_description), description = stringResource(R.string.text_shuffle_languages_description),
checked = shuffleLanguages, checked = shuffleLanguages && shuffleLanguagesEnabled,
onCheckedChange = onShuffleLanguagesChanged onCheckedChange = { enabled ->
if (shuffleLanguagesEnabled) {
onShuffleLanguagesChanged(enabled)
} else {
onShuffleLanguagesChanged(false)
}
}
) )
if (!shuffleLanguagesEnabled) {
Text(
text = stringResource(R.string.text_shuffle_languages_disabled_by_direction),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
OptionItemSwitch( OptionItemSwitch(
title = stringResource(R.string.label_training_mode), title = stringResource(R.string.label_training_mode),
description = stringResource(R.string.text_training_mode_description), description = stringResource(R.string.text_training_mode_description),

View File

@@ -680,6 +680,7 @@
<string name="label_language_direction">Sprachenrichtung <string name="label_language_direction">Sprachenrichtung
</string> </string>
<string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string> <string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string>
<string name="text_language_direction_disabled_with_pairs">Entferne die Sprachpaar-Auswahl, um eine Richtung zu wählen.</string>
<string name="label_guessing_exercise">Raten</string> <string name="label_guessing_exercise">Raten</string>
<string name="label_spelling_exercise">Rechtschreibung</string> <string name="label_spelling_exercise">Rechtschreibung</string>
<string name="label_multiple_choice_exercise">Multiple Choice</string> <string name="label_multiple_choice_exercise">Multiple Choice</string>
@@ -687,6 +688,7 @@
<string name="text_due_today_only_description">Nur Karten anzeigen, die heute fällig sind.</string> <string name="text_due_today_only_description">Nur Karten anzeigen, die heute fällig sind.</string>
<string name="text_shuffle_card_order_description">Kartenmischung</string> <string name="text_shuffle_card_order_description">Kartenmischung</string>
<string name="text_shuffle_languages_description">Mische die Reihenfolge der Sprachen. Beeinflusst nicht deine Sprachrichtungs-Einstellungen.</string> <string name="text_shuffle_languages_description">Mische die Reihenfolge der Sprachen. Beeinflusst nicht deine Sprachrichtungs-Einstellungen.</string>
<string name="text_shuffle_languages_disabled_by_direction">Deaktiviere die Sprachrichtungs-Einstellung, um das Mischen zu aktivieren.</string>
<string name="label_conjugation">Konjugation: %1$s</string> <string name="label_conjugation">Konjugation: %1$s</string>
<string name="label_collapse">Einklappen</string> <string name="label_collapse">Einklappen</string>
<string name="label_expand">Ausklappen</string> <string name="label_expand">Ausklappen</string>

View File

@@ -685,6 +685,7 @@
<string name="text_due_today_only_description">Apenas perguntar cartas que estão a vencer hoje.</string> <string name="text_due_today_only_description">Apenas perguntar cartas que estão a vencer hoje.</string>
<string name="text_shuffle_card_order_description">Embaralhar Ordem das Cartas</string> <string name="text_shuffle_card_order_description">Embaralhar Ordem das Cartas</string>
<string name="text_shuffle_languages_description">Embaralhar qual idioma vem primeiro. Não afeta as preferências de direção do idioma.</string> <string name="text_shuffle_languages_description">Embaralhar qual idioma vem primeiro. Não afeta as preferências de direção do idioma.</string>
<string name="text_shuffle_languages_disabled_by_direction">Desative a preferência de direção do idioma para habilitar o embaralhamento.</string>
<string name="label_conjugation">Conjugação: %1$s</string> <string name="label_conjugation">Conjugação: %1$s</string>
<string name="label_collapse">Recolher</string> <string name="label_collapse">Recolher</string>
<string name="label_expand">Expandir</string> <string name="label_expand">Expandir</string>
@@ -844,6 +845,7 @@
<string name="text_failed_to_fetch_manifest">Falha ao buscar informações de download sobre dicionários disponíveis: %1$s</string> <string name="text_failed_to_fetch_manifest">Falha ao buscar informações de download sobre dicionários disponíveis: %1$s</string>
<string name="text_translation_instructions">Defina o modelo para tradução e dê instruções opcionais sobre como traduzir.</string> <string name="text_translation_instructions">Defina o modelo para tradução e dê instruções opcionais sobre como traduzir.</string>
<string name="text_language_direction_explanation">Você pode definir uma preferência opcional sobre qual idioma deve vir primeiro ou segundo.</string> <string name="text_language_direction_explanation">Você pode definir uma preferência opcional sobre qual idioma deve vir primeiro ou segundo.</string>
<string name="text_language_direction_disabled_with_pairs">Limpe a seleção de pares de idiomas para escolher uma direção.</string>
<string name="label_all_categories">Todas as Categorias</string> <string name="label_all_categories">Todas as Categorias</string>
<string name="text_description_dictionary_prompt">Defina um modelo para gerar conteúdo do dicionário e dê instruções opcionais.</string> <string name="text_description_dictionary_prompt">Defina um modelo para gerar conteúdo do dicionário e dê instruções opcionais.</string>
<string name="hint_vocabulary_progress_hint_title">Acompanhamento de Progresso de Vocabulário</string> <string name="hint_vocabulary_progress_hint_title">Acompanhamento de Progresso de Vocabulário</string>

View File

@@ -824,6 +824,7 @@
<string name="text_label_word">Enter a word\n</string> <string name="text_label_word">Enter a word\n</string>
<string name="text_language_code">Language Code</string> <string name="text_language_code">Language Code</string>
<string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string> <string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string>
<string name="text_language_direction_disabled_with_pairs">Clear language pair selection to choose a direction.</string>
<string name="text_language_options">Language Options</string> <string name="text_language_options">Language Options</string>
<string name="text_last_7_days">Last 7 Days</string> <string name="text_last_7_days">Last 7 Days</string>
<string name="text_let_ai_find_vocabulary_for_you">Let AI find vocabulary for you!</string> <string name="text_let_ai_find_vocabulary_for_you">Let AI find vocabulary for you!</string>
@@ -900,6 +901,7 @@
<string name="text_shuffle_card_order_description">Shuffle Card Order</string> <string name="text_shuffle_card_order_description">Shuffle Card Order</string>
<string name="text_shuffle_languages">Shuffle Languages</string> <string name="text_shuffle_languages">Shuffle Languages</string>
<string name="text_shuffle_languages_description">Shuffle what language comes first. Does not affect language direction preferences.</string> <string name="text_shuffle_languages_description">Shuffle what language comes first. Does not affect language direction preferences.</string>
<string name="text_shuffle_languages_disabled_by_direction">Disable language direction preference to enable shuffling.</string>
<string name="text_shuffle_questions">Shuffle questions</string> <string name="text_shuffle_questions">Shuffle questions</string>
<string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string> <string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string>
<string name="text_stage_2d">Stage %1$s</string> <string name="text_stage_2d">Stage %1$s</string>