From 1b4a387451a0f70341cfdbab75e01453eeca3da2 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 28 Feb 2026 22:25:21 +0000 Subject: [PATCH] Fix #15760: Convert undo/redo labels to sentence case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements sentence case conversion for undo/redo labels to maintain consistency with the rest of the UI. Changes: - Added 120+ sentence case string resources for undo/redo actions - Created helper functions in SentenceCase.kt for undo/redo label conversion - Updated ReviewerViewModel.kt to convert undo/redo labels to sentence case - Updated Undo.kt to convert undo/redo messages to sentence case - Updated CardBrowser.kt, DeckPicker.kt, Reviewer.kt, StudyOptionsActivity.kt to use sentence case for undo labels The conversion handles patterns like: - "Undo Empty Cards" → "Undo empty cards" - "Empty Cards undone" → "Empty cards undone" - "Redo Empty Cards" → "Redo empty cards" --- .../main/java/com/ichi2/anki/CardBrowser.kt | 8 +- .../main/java/com/ichi2/anki/DeckPicker.kt | 10 +- .../src/main/java/com/ichi2/anki/Reviewer.kt | 16 ++- .../com/ichi2/anki/StudyOptionsActivity.kt | 9 +- .../src/main/java/com/ichi2/anki/Undo.kt | 18 ++- .../ui/internationalization/SentenceCase.kt | 127 ++++++++++++++++++ .../ui/windows/reviewer/ReviewerViewModel.kt | 27 +++- .../src/main/res/values/sentence-case.xml | 122 ++++++++++++++++- 8 files changed, 313 insertions(+), 24 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index f15b74d2f086..9631aa6beb27 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -84,7 +84,7 @@ import com.ichi2.anki.libanki.Collection import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.libanki.SortOrder import com.ichi2.anki.libanki.undoAvailable -import com.ichi2.anki.libanki.undoLabel +import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase import com.ichi2.anki.model.CardStateFilter import com.ichi2.anki.model.CardsOrNotes import com.ichi2.anki.model.CardsOrNotes.CARDS @@ -866,7 +866,11 @@ open class CardBrowser : } actionBarMenu?.findItem(R.id.action_undo)?.run { isVisible = getColUnsafe.undoAvailable() - title = getColUnsafe.undoLabel() + title = getColUnsafe.undoLabel()?.let { label -> + label.removePrefix("Undo ").let { action -> + undoLabelToSentenceCase(this@CardBrowser, action) + } + } } actionBarMenu?.findItem(R.id.action_reschedule_cards)?.title = diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 90a655016d64..75a921da658e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -148,7 +148,7 @@ import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.libanki.exception.ConfirmModSchemaException import com.ichi2.anki.libanki.sched.DeckNode import com.ichi2.anki.libanki.undoAvailable -import com.ichi2.anki.libanki.undoLabel +import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase import com.ichi2.anki.mediacheck.MediaCheckFragment import com.ichi2.anki.observability.ChangeManager import com.ichi2.anki.pages.AnkiPackageImporterFragment @@ -702,9 +702,15 @@ open class DeckPicker : fun onUndoUpdated(a: Unit) { launchCatchingTask { withOpenColOrNull { + val rawUndoLabel = undoLabel() + val sentenceCaseLabel = rawUndoLabel?.let { label -> + label.removePrefix("Undo ").let { action -> + undoLabelToSentenceCase(this@DeckPicker, action) + } + } optionsMenuState = optionsMenuState?.copy( - undoLabel = undoLabel(), + undoLabel = sentenceCaseLabel, undoAvailable = undoAvailable(), ) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index a699c6ef9dfe..dfda2032ef94 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -75,11 +75,9 @@ import com.ichi2.anki.libanki.CardId import com.ichi2.anki.libanki.Collection import com.ichi2.anki.libanki.QueueType import com.ichi2.anki.libanki.redoAvailable -import com.ichi2.anki.libanki.redoLabel import com.ichi2.anki.libanki.sched.Counts import com.ichi2.anki.libanki.sched.CurrentQueueState import com.ichi2.anki.libanki.undoAvailable -import com.ichi2.anki.libanki.undoLabel import com.ichi2.anki.multimedia.audio.AudioRecordingController import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.generateTempAudioFile import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.isAudioRecordingSaved @@ -98,6 +96,8 @@ import com.ichi2.anki.reviewer.AnswerButtons.Companion.getTextColors import com.ichi2.anki.reviewer.AnswerTimer import com.ichi2.anki.reviewer.AutomaticAnswerAction import com.ichi2.anki.reviewer.Binding +import com.ichi2.anki.ui.internationalization.redoLabelToSentenceCase +import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase import com.ichi2.anki.reviewer.BindingMap import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.CardMarker @@ -897,7 +897,11 @@ open class Reviewer : if (getColUnsafe.undoAvailable()) { // set the undo title to a named action ('Undo Answer Card' etc...) - undoIcon.title = getColUnsafe.undoLabel() + undoIcon.title = getColUnsafe.undoLabel()?.let { label -> + label.removePrefix("Undo ").let { action -> + undoLabelToSentenceCase(this@Reviewer, action) + } + } } else { // In this case, there is no object word for the verb, "Undo", // so in some languages such as Japanese, which have pre/post-positional particle with the object, @@ -914,7 +918,11 @@ open class Reviewer : menu.findItem(R.id.action_redo)?.apply { if (getColUnsafe.redoAvailable()) { - title = getColUnsafe.redoLabel() + title = getColUnsafe.redoLabel()?.let { label -> + label.removePrefix("Redo ").let { action -> + redoLabelToSentenceCase(this@Reviewer, action) + } + } iconAlpha = Themes.ALPHA_ICON_ENABLED_LIGHT isEnabled = true } else { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsActivity.kt index 7622eedb6b2c..f0d3dae6aaf7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsActivity.kt @@ -29,6 +29,7 @@ import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.CustomStudyAction.Co import com.ichi2.anki.libanki.undoAvailable import com.ichi2.anki.libanki.undoLabel import com.ichi2.anki.observability.ChangeManager +import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.ui.RtlCompliantActionProvider import kotlinx.coroutines.launch @@ -131,9 +132,15 @@ class StudyOptionsActivity : lifecycleScope.launch { val newUndoState = withCol { + val rawLabel = undoLabel() + val sentenceCaseLabel = rawLabel?.let { label -> + label.removePrefix("Undo ").let { action -> + undoLabelToSentenceCase(this@StudyOptionsActivity, action) + } + } UndoState( hasAction = undoAvailable(), - label = undoLabel(), + label = sentenceCaseLabel, ) } if (undoState != newUndoState) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Undo.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Undo.kt index 09a0f6ef54e5..36914093d003 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Undo.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Undo.kt @@ -16,6 +16,7 @@ package com.ichi2.anki +import android.content.Context import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import anki.collection.OpChangesAfterUndo @@ -25,8 +26,10 @@ import com.ichi2.anki.libanki.redoAvailable import com.ichi2.anki.libanki.undoAvailable import com.ichi2.anki.observability.undoableOp import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.ui.internationalization.undoneMessageToSentenceCase +import com.ichi2.anki.ui.internationalization.redoneMessageToSentenceCase -suspend fun tryUndo(): String { +suspend fun tryUndo(context: Context): String { val changes = undoableOp { if (undoAvailable()) { @@ -38,11 +41,13 @@ suspend fun tryUndo(): String { return if (changes.operation.isEmpty()) { TR.actionsNothingToUndo() } else { - TR.undoActionUndone(changes.operation) + // Convert "{Action} undone" to sentence case (e.g., "Empty cards undone") + val message = TR.undoActionUndone(changes.operation) + undoneMessageToSentenceCase(context, changes.operation) } } -suspend fun tryRedo(): String { +suspend fun tryRedo(context: Context): String { val changes = undoableOp { if (redoAvailable()) { @@ -54,14 +59,15 @@ suspend fun tryRedo(): String { return if (changes.operation.isEmpty()) { TR.actionsNothingToRedo() } else { - TR.undoRedoAction(changes.operation) + // Convert "Redo {Action}" to sentence case (e.g., "Redo empty cards") + redoneMessageToSentenceCase(context, changes.operation) } } /** If there's an action pending in the review queue, undo it and show a snackbar */ suspend fun FragmentActivity.undoAndShowSnackbar(duration: Int = Snackbar.LENGTH_SHORT) { withProgress { - val text = tryUndo() + val text = tryUndo(this@undoAndShowSnackbar) showSnackbar(text, duration) } } @@ -73,7 +79,7 @@ suspend fun Fragment.undoAndShowSnackbar(duration: Int = Snackbar.LENGTH_SHORT) suspend fun FragmentActivity.redoAndShowSnackbar(duration: Int = Snackbar.LENGTH_SHORT) { withProgress { - val text = tryRedo() + val text = tryRedo(this@redoAndShowSnackbar) showSnackbar(text, duration) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/internationalization/SentenceCase.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/internationalization/SentenceCase.kt index 0a537e1780df..d6fbdcb495a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/internationalization/SentenceCase.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/internationalization/SentenceCase.kt @@ -48,3 +48,130 @@ fun String.toSentenceCase( if (this.equals(resString, ignoreCase = true)) return resString return this } + +/** + * A map of Title Case action names to their sentence case resource IDs. + * Used for undo/redo label conversion. + */ +private val actionToSentenceCaseMap = mapOf( + "Empty Cards" to R.string.sentence_empty_cards, + "Custom Study" to R.string.sentence_custom_study, + "Set Due Date" to R.string.sentence_set_due_date, + "Suspend Card" to R.string.sentence_suspend_card, + "Answer Card" to R.string.sentence_answer_card, + "Add Deck" to R.string.sentence_add_deck, + "Add Note" to R.string.sentence_add_note, + "Update Tag" to R.string.sentence_update_tag, + "Update Note" to R.string.sentence_update_note, + "Update Card" to R.string.sentence_update_card, + "Update Deck" to R.string.sentence_update_deck, + "Reset Card" to R.string.sentence_reset_card, + "Build Deck" to R.string.sentence_build_deck, + "Add Note Type" to R.string.sentence_add_notetype, + "Remove Note Type" to R.string.sentence_remove_notetype, + "Update Note Type" to R.string.sentence_update_notetype, + "Update Config" to R.string.sentence_update_config, + "Card Info" to R.string.sentence_card_info, + "Previous Card Info" to R.string.sentence_previous_card_info, + "Set Flag" to R.string.sentence_set_flag, + "Auto Advance" to R.string.sentence_auto_advance, + "Bury Card" to R.string.sentence_bury_card, + "Bury Note" to R.string.sentence_bury_note, + "Unbury/Unsuspend" to R.string.sentence_unbury_unsuspend, + "Rename" to R.string.sentence_rename, + "Reposition" to R.string.sentence_reposition, + "Forget Card" to R.string.sentence_forget_card, + "Toggle Load Balancer" to R.string.sentence_toggle_load_balancer, +) + +/** + * Converts an action name from Title Case to sentence case. + * + * @param context The context to access resources + * @param action The action name in Title Case (e.g., "Empty Cards") + * @return The action name in sentence case (e.g., "empty cards") + */ +fun actionToSentenceCase( + context: Context, + action: String, +): String { + val resId = actionToSentenceCaseMap[action] + return if (resId != null) { + context.getString(resId) + } else { + // Fallback: convert to lowercase except first letter + action.replaceFirstChar { it.uppercaseChar() }.let { titleCase -> + titleCase.split(" ").joinToString(" ") { word -> + if (word == titleCase.split(" ").first()) { + word.replaceFirstChar { it.uppercaseChar() } + } else { + word.lowercase() + } + } + } + } +} + +/** + * Converts undo label to sentence case. + * Handles patterns like "Undo Empty Cards" -> "Undo empty cards" + * + * @param context The context to access resources + * @param action The action name (e.g., "Empty Cards") + * @return The sentence case version (e.g., "Undo empty cards") + */ +fun undoLabelToSentenceCase( + context: Context, + action: String, +): String { + val actionSentenceCase = actionToSentenceCase(context, action) + return "Undo $actionSentenceCase" +} + +/** + * Converts redo label to sentence case. + * Handles patterns like "Redo Empty Cards" -> "Redo empty cards" + * + * @param context The context to access resources + * @param action The action name (e.g., "Empty Cards") + * @return The sentence case version (e.g., "Redo empty cards") + */ +fun redoLabelToSentenceCase( + context: Context, + action: String, +): String { + val actionSentenceCase = actionToSentenceCase(context, action) + return "Redo $actionSentenceCase" +} + +/** + * Converts "undone" message to sentence case. + * Handles patterns like "Empty Cards undone" -> "Empty cards undone" + * + * @param context The context to access resources + * @param action The action name (e.g., "Empty Cards") + * @return The sentence case version (e.g., "Empty cards undone") + */ +fun undoneMessageToSentenceCase( + context: Context, + action: String, +): String { + val actionSentenceCase = actionToSentenceCase(context, action) + return "$actionSentenceCase undone" +} + +/** + * Converts "redone" message to sentence case. + * Handles patterns like "Empty Cards redone" -> "Empty cards redone" + * + * @param context The context to access resources + * @param action The action name (e.g., "Empty Cards") + * @return The sentence case version (e.g., "Empty cards redone") + */ +fun redoneMessageToSentenceCase( + context: Context, + action: String, +): String { + val actionSentenceCase = actionToSentenceCase(context, action) + return "$actionSentenceCase redone" +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt index 44fa3868cfc6..1384ad09a2cf 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt @@ -15,6 +15,7 @@ */ package com.ichi2.anki.ui.windows.reviewer +import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import anki.collection.OpChanges @@ -22,6 +23,7 @@ import anki.frontend.SetSchedulingStatesRequest import anki.scheduler.CardAnswer.Rating import com.ichi2.anki.AbstractFlashcardViewer import com.ichi2.anki.AbstractFlashcardViewer.Companion.RESULT_NO_MORE_CARDS +import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.Flag @@ -344,12 +346,14 @@ class ReviewerViewModel( private suspend fun undo() { Timber.v("ReviewerViewModel::undo") - actionFeedbackFlow.emit(tryUndo()) + val context = AnkiDroidApp.instance.applicationContext + actionFeedbackFlow.emit(tryUndo(context)) } private suspend fun redo() { Timber.v("ReviewerViewModel::redo") - actionFeedbackFlow.emit(tryRedo()) + val context = AnkiDroidApp.instance.applicationContext + actionFeedbackFlow.emit(tryRedo(context)) } private suspend fun userAction( @@ -568,8 +572,23 @@ class ReviewerViewModel( private suspend fun updateUndoAndRedoLabels() { Timber.v("ReviewerViewModel::updateUndoAndRedoLabels") - undoLabelFlow.emit(withCol { undoLabel() }) - redoLabelFlow.emit(withCol { redoLabel() }) + val context = AnkiDroidApp.instance.applicationContext + undoLabelFlow.emit( + withCol { undoLabel() }?.let { label -> + // Extract action name from "Undo {Action}" format + label.removePrefix("Undo ").let { action -> + com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase(context, action) + } + } + ) + redoLabelFlow.emit( + withCol { redoLabel() }?.let { label -> + // Extract action name from "Redo {Action}" format + label.removePrefix("Redo ").let { action -> + com.ichi2.anki.ui.internationalization.redoLabelToSentenceCase(context, action) + } + } + ) } private suspend fun updateNextTimes() { diff --git a/AnkiDroid/src/main/res/values/sentence-case.xml b/AnkiDroid/src/main/res/values/sentence-case.xml index 7ca2fdb1f943..7eb1edac709d 100644 --- a/AnkiDroid/src/main/res/values/sentence-case.xml +++ b/AnkiDroid/src/main/res/values/sentence-case.xml @@ -20,11 +20,6 @@ As only English uses Title Case [https://en.wikipedia.org/wiki/Letter_case#], we constant overrides to transform string to sentence case https://github.com/ankidroid/Anki-Android/issues/15760 - -TODO: -col.undoLabel() -col.redoLabel() -undoActionUndone() --> Toggle suspend @@ -57,4 +52,121 @@ undoActionUndone() Current card (browse) Previous card (study) Previous card info + + + Undo empty cards + Undo custom study + Undo set due date + Undo suspend card + Undo answer card + Undo add deck + Undo add note + Undo update tag + Undo update note + Undo update card + Undo update deck + Undo reset card + Undo build deck + Undo add note type + Undo remove note type + Undo update note type + Undo update config + Undo card info + Undo previous card info + Undo set flag + Undo auto advance + Undo bury card + Undo bury note + Undo unbury/unsuspend + Undo rename + Undo reposition + Undo forget card + Undo toggle load balancer + + Redo empty cards + Redo custom study + Redo set due date + Redo suspend card + Redo answer card + Redo add deck + Redo add note + Redo update tag + Redo update note + Redo update card + Redo update deck + Redo reset card + Redo build deck + Redo add note type + Redo remove note type + Redo update note type + Redo update config + Redo card info + Redo previous card info + Redo set flag + Redo auto advance + Redo bury card + Redo bury note + Redo unbury/unsuspend + Redo rename + Redo reposition + Redo forget card + Redo toggle load balancer + + Empty cards undone + Custom study undone + Set due date undone + Suspend card undone + Answer card undone + Add deck undone + Add note undone + Update tag undone + Update note undone + Update card undone + Update deck undone + Reset card undone + Build deck undone + Add note type undone + Remove note type undone + Update note type undone + Update config undone + Card info undone + Previous card info undone + Set flag undone + Auto advance undone + Bury card undone + Bury note undone + Unbury/unsuspend undone + Rename undone + Reposition undone + Forget card undone + Toggle load balancer undone + + Empty cards redone + Custom study redone + Set due date redone + Suspend card redone + Answer card redone + Add deck redone + Add note redone + Update tag redone + Update note redone + Update card redone + Update deck redone + Reset card redone + Build deck redone + Add note type redone + Remove note type redone + Update note type redone + Update config redone + Card info redone + Previous card info redone + Set flag redone + Auto advance redone + Bury card redone + Bury note redone + Unbury/unsuspend redone + Rename redone + Reposition redone + Forget card redone + Toggle load balancer redone \ No newline at end of file