diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 5248f0773..0d37c0847 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -11,6 +11,7 @@ import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat +import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -52,6 +53,8 @@ import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature import org.mozilla.fenix.shopping.ReviewQualityCheckFeature import org.mozilla.fenix.shortcut.PwaOnboardingObserver import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.translations.TranslationsDialogFragment.Companion.SESSION_ID +import org.mozilla.fenix.translations.TranslationsDialogFragment.Companion.TRANSLATION_IN_PROGRESS /** * Fragment used for browsing the web within the main app. @@ -211,6 +214,26 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { owner = viewLifecycleOwner, view = binding.root, ) + + setTranslationFragmentResultListener() + } + + private fun setTranslationFragmentResultListener() { + setFragmentResultListener( + TRANSLATION_IN_PROGRESS, + ) { _, result -> + result.getString(SESSION_ID)?.let { + if (it == getCurrentTab()?.id) { + FenixSnackbar.make( + view = binding.dynamicSnackbarContainer, + duration = Snackbar.LENGTH_LONG, + isDisplayedWithBrowserToolbar = true, + ) + .setText(requireContext().getString(R.string.translation_in_progress_snackbar)) + .show() + } + } + } } private fun initTranslationsAction(context: Context) { diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt index efab01c24..98c341ac3 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import mozilla.components.concept.engine.translate.Language -import mozilla.components.concept.engine.translate.TranslationError import org.mozilla.fenix.theme.FirefoxTheme private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f @@ -127,37 +126,29 @@ internal fun TranslationsOptionsAnimation( } } -@Composable @Suppress("LongParameterList") +@Composable internal fun TranslationsDialog( + translationsDialogState: TranslationsDialogState, learnMoreUrl: String, - showFirstTimeTranslation: Boolean, - translateFromLanguages: List?, - translateToLanguages: List?, - initialFrom: Language? = null, - initialTo: Language? = null, - translationError: TranslationError? = null, + showFirstTime: Boolean = false, onSettingClicked: () -> Unit, onLearnMoreClicked: () -> Unit, - onTranslateButtonClick: () -> Unit, - onNotNowButtonClick: () -> Unit, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: () -> Unit, onFromSelected: (Language) -> Unit, onToSelected: (Language) -> Unit, ) { TranslationsDialogBottomSheet( + translationsDialogState = translationsDialogState, learnMoreUrl = learnMoreUrl, - showFirstTimeTranslation = showFirstTimeTranslation, - translationError = translationError, - translateFromLanguages = translateFromLanguages, - translateToLanguages = translateToLanguages, - initialFrom = initialFrom, - initialTo = initialTo, + showFirstTimeFlow = showFirstTime, onSettingClicked = onSettingClicked, onLearnMoreClicked = onLearnMoreClicked, - onTranslateButtonClicked = onTranslateButtonClick, - onNotNowButtonClicked = onNotNowButtonClick, - onFromSelected = onFromSelected, - onToSelected = onToSelected, + onPositiveButtonClicked = onPositiveButtonClicked, + onNegativeButtonClicked = onNegativeButtonClicked, + onFromDropdownSelected = onFromSelected, + onToDropdownSelected = onToSelected, ) } diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt new file mode 100644 index 000000000..3d1e83819 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.translations + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.translate.initialFromLanguage +import mozilla.components.concept.engine.translate.initialToLanguage +import mozilla.components.lib.state.helpers.AbstractBinding + +/** + * Helper for observing Translation state from [BrowserStore]. + */ +class TranslationsDialogBinding( + browserStore: BrowserStore, + private val translationsDialogStore: TranslationsDialogStore, + private val sessionId: String, + private val getTranslatedPageTitle: (localizedFrom: String?, localizedTo: String?) -> String, +) : AbstractBinding(browserStore) { + + override suspend fun onState(flow: Flow) { + flow.mapNotNull { state -> state.findTab(sessionId) } + .distinctUntilChangedBy { + it.translationsState + } + .collect { sessionState -> + val translationsState = sessionState.translationsState + + val fromSelected = + translationsState.translationEngineState?.initialFromLanguage( + translationsState.supportedLanguages?.fromLanguages, + ) + + fromSelected?.let { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateFromSelectedLanguage( + fromSelected, + ), + ) + } + + val toSelected = + translationsState.translationEngineState?.initialToLanguage( + translationsState.supportedLanguages?.toLanguages, + ) + toSelected?.let { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateToSelectedLanguage( + toSelected, + ), + ) + } + + if ( + toSelected != null && fromSelected != null && + translationsDialogStore.state.translatedPageTitle == null + ) { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslatedPageTitle( + getTranslatedPageTitle( + fromSelected.localizedDisplayName, + toSelected.localizedDisplayName, + ), + ), + ) + } + + val translateFromLanguages = translationsState.supportedLanguages?.fromLanguages + translateFromLanguages?.let { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslateFromLanguages( + translateFromLanguages, + ), + ) + } + + val translateToLanguages = translationsState.supportedLanguages?.toLanguages + translateToLanguages?.let { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslateToLanguages( + translateToLanguages, + ), + ) + } + + if (translationsState.isTranslateProcessing) { + updateStoreIfIsTranslateProcessing() + } + + if (translationsState.isTranslated && !translationsState.isTranslateProcessing) { + updateStoreIfTranslated() + } + + if (translationsState.translationError != null) { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslationError(translationsState.translationError), + ) + } + } + } + + private fun updateStoreIfIsTranslateProcessing() { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslationInProgress( + true, + ), + ) + translationsDialogStore.dispatch( + TranslationsDialogAction.DismissDialog( + dismissDialogState = DismissDialogState.WaitingToBeDismissed, + ), + ) + } + + private fun updateStoreIfTranslated() { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslationInProgress( + false, + ), + ) + + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateTranslated( + true, + ), + ) + + if (translationsDialogStore.state.dismissDialogState == DismissDialogState.WaitingToBeDismissed) { + translationsDialogStore.dispatch( + TranslationsDialogAction.DismissDialog( + dismissDialogState = DismissDialogState.Dismiss, + ), + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt index 18b1f11cf..13aa8a733 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt @@ -67,45 +67,29 @@ private val ICON_SIZE = 24.dp /** * Firefox Translations bottom sheet dialog. * + * @param translationsDialogState The current state of the Translations bottom sheet dialog. * @param learnMoreUrl The learn more link for translations website. - * @param showFirstTimeTranslation Whether translations first flow should be shown. - * @param translateFromLanguages Translation menu items to be shown in the translate from dropdown. - * @param translateToLanguages Translation menu items are to be shown in the translate to dropdown. - * @param initialFrom The initial selection for the translate from dropdown. - * @param initialTo The initial selection for the translate to dropdown. - * @param translationError The type of translation errors that can occur. + * @param showFirstTimeFlow Whether translations first flow should be shown. * @param onSettingClicked Invoked when the user clicks on the settings button. * @param onLearnMoreClicked Invoked when the user clicks on the "Learn More" button. - * @param onTranslateButtonClicked Invoked when the user clicks on the "Translate" button. - * @param onNotNowButtonClicked Invoked when the user clicks on the "Not Now" button. - * @param onFromSelected Invoked when the user selects an item on the from dropdown. - * @param onToSelected Invoked when the user selects an item on the to dropdown. + * @param onPositiveButtonClicked Invoked when the user clicks on the positive button. + * @param onNegativeButtonClicked Invoked when the user clicks on the negative button. + * @param onFromDropdownSelected Invoked when the user selects an item on the from dropdown. + * @param onToDropdownSelected Invoked when the user selects an item on the to dropdown. */ -@Composable @Suppress("LongParameterList") +@Composable fun TranslationsDialogBottomSheet( + translationsDialogState: TranslationsDialogState, learnMoreUrl: String, - showFirstTimeTranslation: Boolean, - translateFromLanguages: List?, - translateToLanguages: List?, - initialFrom: Language? = null, - initialTo: Language? = null, - translationError: TranslationError? = null, + showFirstTimeFlow: Boolean = false, onSettingClicked: () -> Unit, onLearnMoreClicked: () -> Unit, - onTranslateButtonClicked: () -> Unit, - onNotNowButtonClicked: () -> Unit, - onFromSelected: (Language) -> Unit, - onToSelected: (Language) -> Unit, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: () -> Unit, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, ) { - var orientation by remember { mutableIntStateOf(Configuration.ORIENTATION_PORTRAIT) } - - val configuration = LocalConfiguration.current - - LaunchedEffect(configuration) { - snapshotFlow { configuration.orientation }.collect { orientation = it } - } - Column( modifier = Modifier.padding(16.dp), ) { @@ -116,142 +100,347 @@ fun TranslationsDialogBottomSheet( ) TranslationsDialogHeader( - showFirstTimeTranslation = showFirstTimeTranslation, + title = if ( + translationsDialogState.isTranslated && translationsDialogState.translatedPageTitle != null + ) { + translationsDialogState.translatedPageTitle + } else { + getTranslationsDialogTitle( + showFirstTime = showFirstTimeFlow, + ) + }, onSettingClicked = onSettingClicked, ) Spacer(modifier = Modifier.height(8.dp)) - if (showFirstTimeTranslation) { + if (showFirstTimeFlow) { TranslationsDialogInfoMessage( learnMoreUrl = learnMoreUrl, onLearnMoreClicked = onLearnMoreClicked, ) } - translationError?.let { - TranslationErrorWarning(translationError) - } + DialogContentBaseOnTranslationState( + translationsDialogState = translationsDialogState, + learnMoreUrl = learnMoreUrl, + onLearnMoreClicked = onLearnMoreClicked, + onPositiveButtonClicked = onPositiveButtonClicked, + onNegativeButtonClicked = onNegativeButtonClicked, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + ) + } +} +/** + * Dialog content will adapt based on the [TranslationsDialogState]. + * + * @param translationsDialogState The current state of the Translations bottom sheet dialog. + * @param learnMoreUrl The learn more link for translations website. + * @param onLearnMoreClicked Invoked when the user clicks on the learn more button. + * @param onPositiveButtonClicked Invoked when the user clicks on the positive button. + * @param onNegativeButtonClicked Invoked when the user clicks on the negative button. + * @param onFromDropdownSelected Invoked when the user selects an item on the from dropdown. + * @param onToDropdownSelected Invoked when the user selects an item on the to dropdown. + */ +@Composable +private fun DialogContentBaseOnTranslationState( + translationsDialogState: TranslationsDialogState, + learnMoreUrl: String, + onLearnMoreClicked: () -> Unit, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: () -> Unit, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, +) { + if (translationsDialogState.error != null) { + DialogContentAnErrorOccurred( + error = translationsDialogState.error, + learnMoreUrl = learnMoreUrl, + onLearnMoreClicked = onLearnMoreClicked, + fromLanguages = translationsDialogState.fromLanguages, + toLanguages = translationsDialogState.toLanguages, + initialFrom = translationsDialogState.initialFrom, + initialTo = translationsDialogState.initialTo, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + onPositiveButtonClicked = onPositiveButtonClicked, + onNegativeButtonClicked = onNegativeButtonClicked, + ) + } else if (translationsDialogState.isTranslated) { + DialogContentTranslated( + translateToLanguages = translationsDialogState.toLanguages, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + onPositiveButtonClicked = onPositiveButtonClicked, + onNegativeButtonClicked = onNegativeButtonClicked, + positiveButtonType = translationsDialogState.positiveButtonType, + initialTo = translationsDialogState.initialTo, + ) + } else { Spacer(modifier = Modifier.height(14.dp)) - if (translationError !is TranslationError.CouldNotLoadLanguagesError && - translateFromLanguages != null && translateToLanguages != null - ) { - when (orientation) { - Configuration.ORIENTATION_LANDSCAPE -> { - TranslationsDialogContentInLandscapeMode( - translateFromLanguages = translateFromLanguages, - translateToLanguages = translateToLanguages, - initialFrom = initialFrom, - initialTo = initialTo, - onFromSelected = onFromSelected, - onToSelected = onToSelected, - ) - } - - else -> { - TranslationsDialogContentInPortraitMode( - translateFromLanguages = translateFromLanguages, - translateToLanguages = translateToLanguages, - initialFrom = initialFrom, - initialTo = initialTo, - onFromSelected = onFromSelected, - onToSelected = onToSelected, - ) - } - } + TranslationsDialogContent( + translateFromLanguages = translationsDialogState.fromLanguages, + translateToLanguages = translationsDialogState.toLanguages, + initialFrom = translationsDialogState.initialFrom, + initialTo = translationsDialogState.initialTo, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + ) - Spacer(modifier = Modifier.height(16.dp)) - } + Spacer(modifier = Modifier.height(16.dp)) TranslationsDialogActionButtons( - translationError = translationError, - onTranslateButtonClicked = onTranslateButtonClicked, - onNotNowButtonClicked = onNotNowButtonClicked, + positiveButtonText = stringResource(id = R.string.translations_bottom_sheet_positive_button), + negativeButtonText = stringResource(id = R.string.translations_bottom_sheet_negative_button), + positiveButtonType = translationsDialogState.positiveButtonType, + onNegativeButtonClicked = onNegativeButtonClicked, + onPositiveButtonClicked = onPositiveButtonClicked, ) } } +/** + * Dialog content if the web page was translated. + * + * @param translateToLanguages Translation menu items to be shown in the translate to dropdown. + * @param onFromDropdownSelected Invoked when the user selects an item on the from dropdown. + * @param onToDropdownSelected Invoked when the user selects an item on the to dropdown. + * @param onPositiveButtonClicked Invoked when the user clicks on the positive button. + * @param onNegativeButtonClicked Invoked when the user clicks on the negative button. + * @param positiveButtonType Can be enabled,disabled or in progress. If it is null, the button will be disabled. + * @param initialTo Initial "to" language, based on the translation state and page state. + */ @Composable -private fun TranslationsDialogContentInPortraitMode( - translateFromLanguages: List, - translateToLanguages: List, - initialFrom: Language? = null, +private fun DialogContentTranslated( + translateToLanguages: List?, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: () -> Unit, + positiveButtonType: PositiveButtonType? = null, initialTo: Language? = null, - onFromSelected: (Language) -> Unit, - onToSelected: (Language) -> Unit, +) { + Spacer(modifier = Modifier.height(14.dp)) + + TranslationsDialogContent( + translateToLanguages = translateToLanguages, + initialTo = initialTo, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TranslationsDialogActionButtons( + positiveButtonText = stringResource(id = R.string.translations_bottom_sheet_positive_button), + negativeButtonText = stringResource(id = R.string.translations_bottom_sheet_negative_button_restore), + positiveButtonType = positiveButtonType, + onPositiveButtonClicked = onPositiveButtonClicked, + onNegativeButtonClicked = onNegativeButtonClicked, + ) +} +/** + * Dialog content if an [TranslationError] appears during the translation process. + * + * @param error An error that can occur during the translation process. + * @param learnMoreUrl The learn more link for translations website. + * @param onLearnMoreClicked Invoked when the user clicks on the learn more button. + * @param fromLanguages Translation menu items to be shown in the translate from dropdown. + * @param toLanguages Translation menu items to be shown in the translate to dropdown. + * @param initialFrom Initial "from" language, based on the translation state and page state. + * @param initialTo Initial "to" language, based on the translation state and page state. + * @param onFromDropdownSelected Invoked when the user selects an item on the from dropdown. + * @param onToDropdownSelected Invoked when the user selects an item on the to dropdown. + * @param onPositiveButtonClicked Invoked when the user clicks on the positive button. + * @param onNegativeButtonClicked Invoked when the user clicks on the negative button. + */ +@Suppress("LongParameterList") +@Composable +private fun DialogContentAnErrorOccurred( + error: TranslationError, + learnMoreUrl: String, + onLearnMoreClicked: () -> Unit, + fromLanguages: List?, + toLanguages: List?, + initialFrom: Language? = null, + initialTo: Language? = null, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: () -> Unit, ) { - Column { - TranslationsDropdown( - header = stringResource(id = R.string.translations_bottom_sheet_translate_from), - modifier = Modifier.fillMaxWidth(), - translateLanguages = translateFromLanguages, - initiallySelected = initialFrom, - onLanguageSelection = onFromSelected, - ) + TranslationErrorWarning( + error, + learnMoreUrl = learnMoreUrl, + onLearnMoreClicked = onLearnMoreClicked, + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(14.dp)) - TranslationsDropdown( - header = stringResource(id = R.string.translations_bottom_sheet_translate_to), - modifier = Modifier.fillMaxWidth(), - translateLanguages = translateToLanguages, - initiallySelected = initialTo, - onLanguageSelection = onToSelected, + if (error !is TranslationError.CouldNotLoadLanguagesError) { + TranslationsDialogContent( + translateFromLanguages = fromLanguages, + translateToLanguages = toLanguages, + initialFrom = initialFrom, + initialTo = initialTo, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, ) } + + val negativeButtonTitle = if (error is TranslationError.LanguageNotSupportedError) { + stringResource(id = R.string.translations_bottom_sheet_negative_button_error) + } else { + stringResource(id = R.string.translations_bottom_sheet_negative_button) + } + + val positiveButtonTitle = if (error is TranslationError.CouldNotLoadLanguagesError) { + stringResource(id = R.string.translations_bottom_sheet_positive_button_error) + } else { + stringResource(id = R.string.translations_bottom_sheet_positive_button) + } + + val positiveButtonType = if (error is TranslationError.LanguageNotSupportedError) { + PositiveButtonType.Disabled + } else { + PositiveButtonType.Enabled + } + + TranslationsDialogActionButtons( + positiveButtonText = positiveButtonTitle, + negativeButtonText = negativeButtonTitle, + positiveButtonType = positiveButtonType, + onNegativeButtonClicked = onNegativeButtonClicked, + onPositiveButtonClicked = onPositiveButtonClicked, + ) } @Composable -private fun TranslationsDialogContentInLandscapeMode( - translateFromLanguages: List, - translateToLanguages: List, +private fun TranslationsDialogContent( + translateFromLanguages: List? = null, + translateToLanguages: List? = null, + initialFrom: Language? = null, + initialTo: Language? = null, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, +) { + var orientation by remember { mutableIntStateOf(Configuration.ORIENTATION_PORTRAIT) } + + val configuration = LocalConfiguration.current + + LaunchedEffect(configuration) { + snapshotFlow { configuration.orientation }.collect { orientation = it } + } + when (orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + TranslationsDialogContentInLandscapeMode( + translateFromLanguages = translateFromLanguages, + translateToLanguages = translateToLanguages, + initialFrom = initialFrom, + initialTo = initialTo, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + ) + } + + else -> { + TranslationsDialogContentInPortraitMode( + translateFromLanguages = translateFromLanguages, + translateToLanguages = translateToLanguages, + initialFrom = initialFrom, + initialTo = initialTo, + onFromDropdownSelected = onFromDropdownSelected, + onToDropdownSelected = onToDropdownSelected, + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) +} + +@Composable +private fun TranslationsDialogContentInPortraitMode( + translateFromLanguages: List? = null, + translateToLanguages: List? = null, initialFrom: Language? = null, initialTo: Language? = null, - onFromSelected: (Language) -> Unit, - onToSelected: (Language) -> Unit, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, ) { Column { - Row { + translateFromLanguages?.let { TranslationsDropdown( header = stringResource(id = R.string.translations_bottom_sheet_translate_from), - modifier = Modifier.weight(1f), - isInLandscapeMode = true, + modifier = Modifier.fillMaxWidth(), + isInLandscapeMode = false, translateLanguages = translateFromLanguages, initiallySelected = initialFrom, - onLanguageSelection = onFromSelected, + onLanguageSelection = onFromDropdownSelected, ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) + } + translateToLanguages?.let { TranslationsDropdown( header = stringResource(id = R.string.translations_bottom_sheet_translate_to), - modifier = Modifier.weight(1f), - isInLandscapeMode = true, - translateLanguages = translateToLanguages, + modifier = Modifier.fillMaxWidth(), + isInLandscapeMode = false, + translateLanguages = it, initiallySelected = initialTo, - onLanguageSelection = onToSelected, + onLanguageSelection = onToDropdownSelected, ) } } } @Composable -private fun TranslationsDialogHeader( - showFirstTimeTranslation: Boolean, - onSettingClicked: () -> Unit, +private fun TranslationsDialogContentInLandscapeMode( + translateFromLanguages: List? = null, + translateToLanguages: List? = null, + initialFrom: Language? = null, + initialTo: Language? = null, + onFromDropdownSelected: (Language) -> Unit, + onToDropdownSelected: (Language) -> Unit, ) { - val title: String = if (showFirstTimeTranslation) { - stringResource( - id = R.string.translations_bottom_sheet_title_first_time, - stringResource(id = R.string.firefox), - ) - } else { - stringResource(id = R.string.translations_bottom_sheet_title) + Column { + Row { + translateFromLanguages?.let { + TranslationsDropdown( + header = stringResource(id = R.string.translations_bottom_sheet_translate_from), + modifier = Modifier.weight(1f), + isInLandscapeMode = true, + translateLanguages = translateFromLanguages, + initiallySelected = initialFrom, + onLanguageSelection = onFromDropdownSelected, + ) + + Spacer(modifier = Modifier.width(16.dp)) + } + + translateToLanguages?.let { + TranslationsDropdown( + header = stringResource(id = R.string.translations_bottom_sheet_translate_to), + modifier = Modifier.weight(1f), + isInLandscapeMode = true, + translateLanguages = it, + initiallySelected = initialTo, + onLanguageSelection = onToDropdownSelected, + ) + } + } } +} +@Composable +private fun TranslationsDialogHeader( + title: String, + onSettingClicked: () -> Unit, +) { Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -280,7 +469,11 @@ private fun TranslationsDialogHeader( } @Composable -private fun TranslationErrorWarning(translationError: TranslationError) { +private fun TranslationErrorWarning( + translationError: TranslationError, + learnMoreUrl: String, + onLearnMoreClicked: () -> Unit, +) { val modifier = Modifier .padding(top = 8.dp) .fillMaxWidth() @@ -303,6 +496,9 @@ private fun TranslationErrorWarning(translationError: TranslationError) { } is TranslationError.LanguageNotSupportedError -> { + val learnMoreText = + stringResource(id = R.string.translation_error_language_not_supported_learn_more) + ReviewQualityCheckInfoCard( title = stringResource( id = R.string.translation_error_language_not_supported_warning_text, @@ -313,9 +509,9 @@ private fun TranslationErrorWarning(translationError: TranslationError) { footer = stringResource( id = R.string.translation_error_language_not_supported_learn_more, ) to LinkTextState( - text = stringResource(id = R.string.translation_error_language_not_supported_learn_more), - url = "https://www.mozilla.org", - onClick = {}, + text = learnMoreText, + url = learnMoreUrl, + onClick = { onLearnMoreClicked() }, ), ) } @@ -353,22 +549,13 @@ private fun TranslationsDialogInfoMessage( } } -/** - * Creates a dropdown with language selection to use to select languages for translation. - * - * @param header The title of the dropdown. - * @param translateLanguages The language choices the dropdown should provide. - * @param modifier Any modifiers for the component. - * @param isInLandscapeMode If the item should layout for landscape mode. - * @param initiallySelected The language initially selected, if null will show "Choose a language". - * @param onLanguageSelection Callback for the selected language. - */ +@Suppress("LongMethod") @Composable private fun TranslationsDropdown( header: String, translateLanguages: List, modifier: Modifier = Modifier, - isInLandscapeMode: Boolean = false, + isInLandscapeMode: Boolean, initiallySelected: Language? = null, onLanguageSelection: (Language) -> Unit, ) { @@ -396,7 +583,8 @@ private fun TranslationsDropdown( Spacer(modifier = Modifier.height(4.dp)) - var initialValue = stringResource(R.string.translations_bottom_sheet_default_dropdown_selection) + var initialValue = + stringResource(R.string.translations_bottom_sheet_default_dropdown_selection) initiallySelected?.localizedDisplayName?.let { initialValue = it } @@ -423,19 +611,21 @@ private fun TranslationsDropdown( onDismissRequest = { expanded = false }, - menuItems = getContextMenuItems( translateLanguages = translateLanguages, - onClickItem = onLanguageSelection, + selectedLanguage = initiallySelected, + onClickItem = { + onLanguageSelection(it) + }, ), - modifier = Modifier .onGloballyPositioned { coordinates -> contextMenuWidthDp = with(density) { coordinates.size.width.toDp() } } - .requiredSizeIn(maxHeight = 200.dp), + .requiredSizeIn(maxHeight = 200.dp) + .padding(horizontal = if (initiallySelected == null) 36.dp else 4.dp), offset = if (isInLandscapeMode) { DpOffset( -contextMenuWidthDp + ICON_SIZE, @@ -455,8 +645,21 @@ private fun TranslationsDropdown( } } +@Composable +private fun getTranslationsDialogTitle( + showFirstTime: Boolean = false, +) = if (showFirstTime) { + stringResource( + id = R.string.translations_bottom_sheet_title_first_time, + stringResource(id = R.string.firefox), + ) +} else { + stringResource(id = R.string.translations_bottom_sheet_title) +} + private fun getContextMenuItems( translateLanguages: List, + selectedLanguage: Language? = null, onClickItem: (Language) -> Unit, ): List { val menuItems = mutableListOf() @@ -465,6 +668,7 @@ private fun getContextMenuItems( menuItems.add( MenuItem( title = it, + isChecked = item == selectedLanguage, onClick = { onClickItem(item) }, @@ -477,64 +681,52 @@ private fun getContextMenuItems( @Composable private fun TranslationsDialogActionButtons( - translationError: TranslationError? = null, - onTranslateButtonClicked: () -> Unit, - onNotNowButtonClicked: () -> Unit, + positiveButtonText: String, + negativeButtonText: String, + positiveButtonType: PositiveButtonType? = null, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: () -> Unit, ) { - val isTranslationInProgress = remember { mutableStateOf(false) } - Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - val negativeButtonTitle = - if (translationError is TranslationError.LanguageNotSupportedError) { - stringResource(id = R.string.translations_bottom_sheet_negative_button_error) - } else { - stringResource(id = R.string.translations_bottom_sheet_negative_button) - } - TextButton( - text = negativeButtonTitle, + text = negativeButtonText, modifier = Modifier, - onClick = onNotNowButtonClicked, + onClick = onNegativeButtonClicked, ) Spacer(modifier = Modifier.width(10.dp)) - if (isTranslationInProgress.value) { - DownloadIndicator( - text = stringResource(id = R.string.translations_bottom_sheet_translating_in_progress), - contentDescription = stringResource( - id = R.string.translations_bottom_sheet_translating_in_progress_content_description, - ), - icon = painterResource(id = R.drawable.mozac_ic_sync_24), - ) - } else { - val positiveButtonTitle = - if (translationError is TranslationError.CouldNotLoadLanguagesError) { - stringResource(id = R.string.translations_bottom_sheet_positive_button_error) - } else { - stringResource(id = R.string.translations_bottom_sheet_positive_button) - } + when (positiveButtonType) { + PositiveButtonType.InProgress -> { + DownloadIndicator( + text = positiveButtonText, + contentDescription = stringResource( + id = R.string.translations_bottom_sheet_translating_in_progress_content_description, + ), + icon = painterResource(id = R.drawable.mozac_ic_sync_24), + ) + } - if (translationError is TranslationError.LanguageNotSupportedError) { - TertiaryButton( - text = positiveButtonTitle, - enabled = false, + PositiveButtonType.Enabled -> { + PrimaryButton( + text = positiveButtonText, modifier = Modifier.wrapContentSize(), ) { - isTranslationInProgress.value = true - onTranslateButtonClicked() + onPositiveButtonClicked() } - } else { - PrimaryButton( - text = positiveButtonTitle, + } + + else -> { + TertiaryButton( + text = positiveButtonText, + enabled = false, modifier = Modifier.wrapContentSize(), ) { - isTranslationInProgress.value = true - onTranslateButtonClicked() + onPositiveButtonClicked() } } } @@ -546,19 +738,19 @@ private fun TranslationsDialogActionButtons( private fun TranslationsDialogBottomSheetPreview() { FirefoxTheme { TranslationsDialogBottomSheet( + translationsDialogState = TranslationsDialogState( + positiveButtonType = PositiveButtonType.Enabled, + toLanguages = getTranslateToLanguageList(), + fromLanguages = getTranslateFromLanguageList(), + ), learnMoreUrl = "", - showFirstTimeTranslation = true, - translationError = TranslationError.LanguageNotSupportedError(null), - translateFromLanguages = getTranslateFromLanguageList(), - translateToLanguages = getTranslateToLanguageList(), - initialFrom = null, - initialTo = null, + showFirstTimeFlow = true, onSettingClicked = {}, onLearnMoreClicked = {}, - onTranslateButtonClicked = {}, - onNotNowButtonClicked = {}, - onToSelected = {}, - onFromSelected = {}, + onPositiveButtonClicked = {}, + onNegativeButtonClicked = {}, + onFromDropdownSelected = {}, + onToDropdownSelected = {}, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt index e94bc22ce..b28fcbb8e 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt @@ -5,11 +5,13 @@ package org.mozilla.fenix.translations import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -18,18 +20,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import mozilla.components.browser.state.action.TranslationsAction -import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.translate.TranslationOperation -import mozilla.components.concept.engine.translate.initialFromLanguage -import mozilla.components.concept.engine.translate.initialToLanguage import mozilla.components.lib.state.ext.observeAsComposableState +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -53,6 +54,9 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { private var behavior: BottomSheetBehavior? = null private val args by navArgs() private val browserStore: BrowserStore by lazy { requireComponents.core.store } + private val translationDialogBinding = ViewBoundFeatureWrapper() + private lateinit var translationsDialogStore: TranslationsDialogStore + private var isTranslationInProgress: Boolean? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = super.onCreateDialog(savedInstanceState).apply { @@ -70,40 +74,21 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View = ComposeView(requireContext()).apply { - // Signalling need to fetch languages - browserStore.dispatch( - TranslationsAction.OperationRequestedAction( - tabId = args.sessionId, - operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES, + translationsDialogStore = TranslationsDialogStore( + TranslationsDialogState(), + listOf( + TranslationsDialogMiddleware( + browserStore = browserStore, + sessionId = args.sessionId, + ), ), ) - + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val translationsState = browserStore.observeAsComposableState { - state -> - state.findTab(args.sessionId) - ?.translationsState - }.value - - var fromSelected by remember { - mutableStateOf( - translationsState?.translationEngineState - ?.initialFromLanguage(translationsState.supportedLanguages?.fromLanguages), - ) - } - - var toSelected by remember { - mutableStateOf( - translationsState?.translationEngineState - ?.initialToLanguage(translationsState.supportedLanguages?.toLanguages), - ) - } - FirefoxTheme { var translationsVisibility by remember { mutableStateOf( - args.translationsDialogAccessPoint == - TranslationsDialogAccessPoint.Translations, + args.translationsDialogAccessPoint == TranslationsDialogAccessPoint.Translations, ) } @@ -139,46 +124,15 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { }, ) { val learnMoreUrl = SupportUtils.getSumoURLForTopic( - context, + requireContext(), SupportUtils.SumoTopic.TRANSLATIONS, ) - TranslationsDialog( + + TranslationsDialogContent( learnMoreUrl = learnMoreUrl, - showFirstTimeTranslation = context.settings().showFirstTimeTranslation, - translateFromLanguages = translationsState?.supportedLanguages?.fromLanguages, - translateToLanguages = translationsState?.supportedLanguages?.toLanguages, - initialFrom = fromSelected, - initialTo = toSelected, - onSettingClicked = { - translationsVisibility = false - }, - onLearnMoreClicked = { - (requireActivity() as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = learnMoreUrl, - newTab = true, - from = BrowserDirection.FromTranslationsDialogFragment, - ) - }, - onTranslateButtonClick = { - fromSelected?.code?.let { fromLanguage -> - toSelected?.code?.let { toLanguage -> - TranslationsAction.TranslateAction( - tabId = args.sessionId, - fromLanguage = fromLanguage, - toLanguage = toLanguage, - options = null, - ) - } - }?.let { - browserStore.dispatch( - it, - ) - } - }, - onNotNowButtonClick = { dismiss() }, - onFromSelected = { fromSelected = it }, - onToSelected = { toSelected = it }, - ) + ) { + translationsVisibility = false + } } } } @@ -217,4 +171,102 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { } } } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + translationDialogBinding.set( + feature = TranslationsDialogBinding( + browserStore = browserStore, + translationsDialogStore = translationsDialogStore, + sessionId = args.sessionId, + getTranslatedPageTitle = { localizedFrom, localizedTo -> + requireContext().getString( + R.string.translations_bottom_sheet_title_translation_completed, + localizedFrom, + localizedTo, + ) + }, + ), + owner = this, + view = view, + ) + translationsDialogStore.dispatch(TranslationsDialogAction.InitTranslationsDialog) + } + + @Composable + private fun TranslationsDialogContent(learnMoreUrl: String, onSettingClicked: () -> Unit) { + val translationsDialogState = + translationsDialogStore.observeAsComposableState { it }.value + + translationsDialogState?.let { state -> + isTranslationInProgress = state.isTranslationInProgress + + if (state.dismissDialogState is DismissDialogState.Dismiss) { + dismissDialog() + } + + TranslationsDialog( + translationsDialogState = translationsDialogState, + learnMoreUrl = learnMoreUrl, + showFirstTime = requireContext().settings().showFirstTimeTranslation, + onSettingClicked = onSettingClicked, + onLearnMoreClicked = { openBrowserAndLoad(learnMoreUrl) }, + onPositiveButtonClicked = { + translationsDialogStore.dispatch(TranslationsDialogAction.TranslateAction) + }, + onNegativeButtonClicked = { + if (state.isTranslated) { + translationsDialogStore.dispatch(TranslationsDialogAction.RestoreTranslation) + } + dismiss() + }, + onFromSelected = { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateFromSelectedLanguage( + it, + ), + ) + }, + onToSelected = { + translationsDialogStore.dispatch( + TranslationsDialogAction.UpdateToSelectedLanguage( + it, + ), + ) + }, + ) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (isTranslationInProgress == true) { + setFragmentResult( + TRANSLATION_IN_PROGRESS, + bundleOf( + SESSION_ID to args.sessionId, + ), + ) + } + } + + private fun openBrowserAndLoad(learnMoreUrl: String) { + (requireActivity() as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = learnMoreUrl, + newTab = true, + from = BrowserDirection.FromTranslationsDialogFragment, + ) + } + + private fun dismissDialog() { + if (requireContext().settings().showFirstTimeTranslation) { + requireContext().settings().showFirstTimeTranslation = false + } + dismiss() + } + + companion object { + const val TRANSLATION_IN_PROGRESS = "translationInProgress" + const val SESSION_ID = "sessionId" + } } diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt new file mode 100644 index 000000000..39819732e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.translations + +import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.translate.TranslationOperation +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext + +/** + * [Middleware] implementation for updating [BrowserStore] based on translation actions. + */ +class TranslationsDialogMiddleware( + private val browserStore: BrowserStore, + private val sessionId: String, +) : Middleware { + + override fun invoke( + context: MiddlewareContext, + next: (TranslationsDialogAction) -> Unit, + action: TranslationsDialogAction, + ) { + when (action) { + is TranslationsDialogAction.FetchSupportedLanguages -> { + browserStore.dispatch( + TranslationsAction.OperationRequestedAction( + tabId = sessionId, + operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES, + ), + ) + } + + is TranslationsDialogAction.TranslateAction -> { + context.state.initialFrom?.code?.let { fromLanguage -> + context.state.initialTo?.code?.let { toLanguage -> + TranslationsAction.TranslateAction( + tabId = sessionId, + fromLanguage = fromLanguage, + toLanguage = toLanguage, + options = null, + ) + } + }?.let { + browserStore.dispatch( + it, + ) + } + } + + is TranslationsDialogAction.RestoreTranslation -> { + browserStore.dispatch(TranslationsAction.TranslateRestoreAction(sessionId)) + } + + else -> { + next(action) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogStore.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogStore.kt new file mode 100644 index 000000000..8fb5cd0ce --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogStore.kt @@ -0,0 +1,285 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.fenix.translations + +import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.translate.Language +import mozilla.components.concept.engine.translate.TranslationError +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * The [TranslationsDialogStore] holds the [TranslationsDialogState] (state tree). + * + * The only way to change the [TranslationsDialogState] inside + * [TranslationsDialogStore] is to dispatch an [Action] on it. + */ +class TranslationsDialogStore( + initialState: TranslationsDialogState, + middlewares: List> = emptyList(), +) : Store( + initialState, + TranslationsDialogReducer::reduce, + middlewares, +) { + init { + dispatch(TranslationsDialogAction.FetchSupportedLanguages) + } +} + +/** + * The current state of the Translations bottom sheet dialog. + * + * @property isTranslated The page is currently translated. + * @property isTranslationInProgress The page is currently attempting a translation. + * @property positiveButtonType Can be enabled,disabled or in progress. + * @property error An error that can occur during the translation process. + * @property dismissDialogState Whether the dialog bottom sheet should be dismissed. + * @property initialFrom Initial "from" language, based on the translation state and page state. + * @property initialTo Initial "to" language, based on the translation state and page state. + * @property fromLanguages Translation menu items to be shown in the translate from dropdown. + * @property toLanguages Translation menu items to be shown in the translate to dropdown. + * @property translatedPageTitle Title of the bottom sheet dialogue if the page was translated. + */ +data class TranslationsDialogState( + var isTranslated: Boolean = false, + val isTranslationInProgress: Boolean = false, + val positiveButtonType: PositiveButtonType? = null, + val error: TranslationError? = null, + val dismissDialogState: DismissDialogState? = null, + val initialFrom: Language? = null, + val initialTo: Language? = null, + val fromLanguages: List? = null, + val toLanguages: List? = null, + val translatedPageTitle: String? = null, +) : State + +/** + * Action to dispatch through the `TranslationsStore` to modify `TranslationsDialogState` through the reducer. + */ +sealed class TranslationsDialogAction : Action { + /** + * Invoked when the [TranslationsDialogStore] is added to the fragment. + */ + object InitTranslationsDialog : TranslationsDialogAction() + + /** + * When FetchSupportedLanguages is dispatched, an [TranslationsAction.OperationRequestedAction] + * will be dispatched to the [BrowserStore] + */ + object FetchSupportedLanguages : TranslationsDialogAction() + + /** + * Invoked when the user wants to translate a website. + */ + object TranslateAction : TranslationsDialogAction() + + /** + * Invoked when the user wants to restore the website to its original pre-translated content. + */ + object RestoreTranslation : TranslationsDialogAction() + + /** + * Invoked when a translation error occurs during the translation process. + */ + data class UpdateTranslationError( + val translationError: TranslationError? = null, + ) : TranslationsDialogAction() + + /** + * Updates translate from languages list. + */ + data class UpdateTranslateFromLanguages( + val translateFromLanguages: List, + ) : TranslationsDialogAction() + + /** + * Updates translate to languages list. + */ + data class UpdateTranslateToLanguages( + val translateToLanguages: List, + ) : TranslationsDialogAction() + + /** + * Updates to the current selected language from the "translateFromLanguages" list. + */ + data class UpdateFromSelectedLanguage( + val language: Language, + ) : TranslationsDialogAction() + + /** + * Updates to the current selected language from the "translateToLanguages" list. + */ + data class UpdateToSelectedLanguage( + val language: Language, + ) : TranslationsDialogAction() + + /** + * Dismiss the translation dialog fragment. + */ + data class DismissDialog( + val dismissDialogState: DismissDialogState, + ) : TranslationsDialogAction() + + /** + * Updates the dialog content to translation in progress status. + */ + data class UpdateTranslationInProgress( + val inProgress: Boolean, + ) : TranslationsDialogAction() + + /** + * Updates the dialog content to translated status. + */ + data class UpdateTranslated( + val isTranslated: Boolean, + ) : TranslationsDialogAction() + + /** + * Updates the dialog title if the page was translated. + */ + data class UpdateTranslatedPageTitle(val title: String) : TranslationsDialogAction() +} + +/** + * Positive button type from the translation bottom sheet. + */ +sealed class PositiveButtonType { + /** + * The translating indicator will appear. + */ + object InProgress : PositiveButtonType() + + /** + * The button is in a disabled state. + */ + object Disabled : PositiveButtonType() + + /** + * The button is in a enabled state. + */ + object Enabled : PositiveButtonType() +} + +/** + * Dismiss translation bottom sheet type. + */ +sealed class DismissDialogState { + /** + * The dialog should be dismissed. + */ + object Dismiss : DismissDialogState() + + /** + * This is the step when translation is in progress and the dialog is waiting to be dismissed. + */ + object WaitingToBeDismissed : DismissDialogState() +} + +internal object TranslationsDialogReducer { + /** + * Reduces the translations dialog state from the current state and an action performed on it. + * + * @param state The current translations dialog state. + * @param action The action to perform. + * @return The new [TranslationsDialogState]. + */ + @Suppress("LongMethod") + fun reduce( + state: TranslationsDialogState, + action: TranslationsDialogAction, + ): TranslationsDialogState { + return when (action) { + is TranslationsDialogAction.UpdateFromSelectedLanguage -> { + state.copy( + initialFrom = action.language, + positiveButtonType = if (state.initialTo != null && action.language != state.initialTo) { + PositiveButtonType.Enabled + } else { + PositiveButtonType.Disabled + }, + ) + } + + is TranslationsDialogAction.UpdateToSelectedLanguage -> { + state.copy( + initialTo = action.language, + positiveButtonType = if (state.initialFrom != null && action.language != state.initialFrom) { + PositiveButtonType.Enabled + } else { + PositiveButtonType.Disabled + }, + ) + } + + is TranslationsDialogAction.UpdateTranslateToLanguages -> { + state.copy(toLanguages = action.translateToLanguages) + } + + is TranslationsDialogAction.UpdateTranslateFromLanguages -> { + state.copy(fromLanguages = action.translateFromLanguages) + } + + is TranslationsDialogAction.DismissDialog -> { + state.copy(dismissDialogState = action.dismissDialogState) + } + + is TranslationsDialogAction.UpdateTranslationInProgress -> { + state.copy( + isTranslationInProgress = action.inProgress, + positiveButtonType = if (action.inProgress) { + PositiveButtonType.InProgress + } else { + state.positiveButtonType + }, + ) + } + + is TranslationsDialogAction.InitTranslationsDialog -> { + state.copy( + positiveButtonType = if (state.initialTo == null || state.initialFrom == null) { + PositiveButtonType.Disabled + } else { + state.positiveButtonType + }, + ) + } + + is TranslationsDialogAction.UpdateTranslationError -> { + state.copy( + error = action.translationError, + positiveButtonType = if ( + action.translationError is TranslationError.LanguageNotSupportedError + ) { + PositiveButtonType.Disabled + } else { + PositiveButtonType.Enabled + }, + ) + } + + is TranslationsDialogAction.UpdateTranslated -> { + state.copy( + isTranslated = action.isTranslated, + positiveButtonType = PositiveButtonType.Disabled, + ) + } + + is TranslationsDialogAction.UpdateTranslatedPageTitle -> { + state.copy(translatedPageTitle = action.title) + } + + is TranslationsDialogAction.TranslateAction, + TranslationsDialogAction.FetchSupportedLanguages, + TranslationsDialogAction.RestoreTranslation, + -> { + // handled by [TranslationsDialogMiddleware] + state + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1a749708..8f14076d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2322,6 +2322,10 @@ Translate this page? + + Page translated from %1$s to %2$s Try private translations in %1$s @@ -2334,6 +2338,8 @@ Translate to Not now + + Show original Done @@ -2354,6 +2360,8 @@ Sorry, we don’t support %1$s yet. Learn more + + Translating… diff --git a/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogBindingTest.kt b/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogBindingTest.kt new file mode 100644 index 000000000..4f0bc4590 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogBindingTest.kt @@ -0,0 +1,308 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.translations + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.translate.DetectedLanguages +import mozilla.components.concept.engine.translate.Language +import mozilla.components.concept.engine.translate.TranslationEngineState +import mozilla.components.concept.engine.translate.TranslationError +import mozilla.components.concept.engine.translate.TranslationOperation +import mozilla.components.concept.engine.translate.TranslationPair +import mozilla.components.concept.engine.translate.TranslationSupport +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.rule.runTestOnMain +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mozilla.fenix.R + +@RunWith(AndroidJUnit4::class) +class TranslationsDialogBindingTest { + @get:Rule + val coroutineRule = MainCoroutineRule() + + lateinit var browserStore: BrowserStore + private lateinit var translationsDialogStore: TranslationsDialogStore + + private val tabId = "1" + private val tab = createTab(url = tabId, id = tabId) + + @Test + fun `WHEN fromLanguage and toLanguage get updated in the browserStore THEN translations dialog actions dispatched with the update`() = + runTestOnMain { + val englishLanguage = Language("en", "English") + val spanishLanguage = Language("es", "Spanish") + translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + ), + ) + + val binding = TranslationsDialogBinding( + browserStore = browserStore, + translationsDialogStore = translationsDialogStore, + sessionId = tabId, + getTranslatedPageTitle = { localizedFrom, localizedTo -> + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + localizedFrom, + localizedTo, + ) + }, + ) + binding.start() + + val detectedLanguages = DetectedLanguages( + documentLangTag = englishLanguage.code, + supportedDocumentLang = true, + userPreferredLangTag = spanishLanguage.code, + ) + + val translationEngineState = TranslationEngineState( + detectedLanguages = detectedLanguages, + error = null, + isEngineReady = true, + requestedTranslationPair = TranslationPair( + fromLanguage = englishLanguage.code, + toLanguage = spanishLanguage.code, + ), + ) + + val supportLanguages = TranslationSupport( + fromLanguages = listOf(englishLanguage), + toLanguages = listOf(spanishLanguage), + ) + + browserStore.dispatch( + TranslationsAction.SetSupportedLanguagesAction( + tabId = tab.id, + supportedLanguages = supportLanguages, + ), + ).joinBlocking() + + browserStore.dispatch( + TranslationsAction.TranslateStateChangeAction( + tabId = tabId, + translationEngineState = translationEngineState, + ), + ).joinBlocking() + + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateFromSelectedLanguage( + englishLanguage, + ), + ) + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateToSelectedLanguage( + spanishLanguage, + ), + ) + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslatedPageTitle( + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + englishLanguage.localizedDisplayName, + spanishLanguage.localizedDisplayName, + ), + ), + ) + } + + @Test + fun `WHEN translate action is sent to the browserStore THEN update translation dialog store based on operation`() = + runTestOnMain { + val englishLanguage = Language("en", "English") + val spanishLanguage = Language("es", "Spanish") + translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + ), + ) + + val binding = TranslationsDialogBinding( + browserStore = browserStore, + translationsDialogStore = translationsDialogStore, + sessionId = tabId, + getTranslatedPageTitle = { localizedFrom, localizedTo -> + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + localizedFrom, + localizedTo, + ) + }, + ) + binding.start() + + browserStore.dispatch( + TranslationsAction.TranslateAction( + tabId = tabId, + fromLanguage = englishLanguage.code, + toLanguage = spanishLanguage.code, + null, + ), + ).joinBlocking() + + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslationInProgress( + true, + ), + ) + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.DismissDialog( + dismissDialogState = DismissDialogState.WaitingToBeDismissed, + ), + ) + } + + @Test + fun `WHEN translate from languages list and translate to languages list are sent to the browserStore THEN update translation dialog store based on operation`() = + runTestOnMain { + translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + ), + ) + + val binding = TranslationsDialogBinding( + browserStore = browserStore, + translationsDialogStore = translationsDialogStore, + sessionId = tabId, + getTranslatedPageTitle = { localizedFrom, localizedTo -> + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + localizedFrom, + localizedTo, + ) + }, + ) + binding.start() + + val toLanguage = Language("de", "German") + val fromLanguage = Language("es", "Spanish") + val supportedLanguages = TranslationSupport(listOf(fromLanguage), listOf(toLanguage)) + browserStore.dispatch( + TranslationsAction.SetSupportedLanguagesAction( + tabId = tab.id, + supportedLanguages = supportedLanguages, + ), + ).joinBlocking() + + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslateFromLanguages( + listOf(fromLanguage), + ), + ) + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslateToLanguages( + listOf(toLanguage), + ), + ) + } + + @Test + fun `WHEN translate action success is sent to the browserStore THEN update translation dialog store based on operation`() = + runTestOnMain { + translationsDialogStore = + spy(TranslationsDialogStore(TranslationsDialogState(dismissDialogState = DismissDialogState.WaitingToBeDismissed))) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + ), + ) + + val binding = TranslationsDialogBinding( + browserStore = browserStore, + translationsDialogStore = translationsDialogStore, + sessionId = tabId, + getTranslatedPageTitle = { localizedFrom, localizedTo -> + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + localizedFrom, + localizedTo, + ) + }, + ) + binding.start() + + browserStore.dispatch( + TranslationsAction.TranslateSuccessAction( + tabId = tab.id, + operation = TranslationOperation.TRANSLATE, + ), + ).joinBlocking() + + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslated( + true, + ), + ) + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslationInProgress( + false, + ), + ) + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.DismissDialog( + dismissDialogState = DismissDialogState.Dismiss, + ), + ) + } + + @Test + fun `WHEN translate fetch error is sent to the browserStore THEN update translation dialog store based on operation`() = + runTestOnMain { + translationsDialogStore = + spy(TranslationsDialogStore(TranslationsDialogState())) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + ), + ) + + val binding = TranslationsDialogBinding( + browserStore = browserStore, + translationsDialogStore = translationsDialogStore, + sessionId = tabId, + getTranslatedPageTitle = { localizedFrom, localizedTo -> + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + localizedFrom, + localizedTo, + ) + }, + ) + binding.start() + + val fetchError = TranslationError.CouldNotLoadLanguagesError(null) + browserStore.dispatch( + TranslationsAction.TranslateExceptionAction( + tabId = tab.id, + operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES, + translationError = fetchError, + ), + ).joinBlocking() + + verify(translationsDialogStore).dispatch( + TranslationsDialogAction.UpdateTranslationError(fetchError), + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogMiddlewareTest.kt new file mode 100644 index 000000000..178b4ec86 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogMiddlewareTest.kt @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.translations + +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.translate.Language +import mozilla.components.concept.engine.translate.TranslationOperation +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class TranslationsDialogMiddlewareTest { + + @Test + fun `GIVEN translationState WHEN FetchSupportedLanguages action is called THEN call OperationRequestedAction from BrowserStore`() = + runTest { + val browserStore = mockk(relaxed = true) + val translationsDialogMiddleware = + TranslationsDialogMiddleware(browserStore = browserStore, sessionId = "tab1") + + val translationStore = TranslationsDialogStore( + initialState = TranslationsDialogState(), + middlewares = listOf(translationsDialogMiddleware), + ) + translationStore.dispatch(TranslationsDialogAction.FetchSupportedLanguages).joinBlocking() + + translationStore.waitUntilIdle() + + verify { + browserStore.dispatch( + TranslationsAction.OperationRequestedAction( + tabId = "tab1", + operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES, + ), + ) + } + } + + @Test + fun `GIVEN translationState WHEN TranslateAction from TranslationDialogStore is called THEN call TranslateAction from BrowserStore`() = + runTest { + val browserStore = mockk(relaxed = true) + val translationsDialogMiddleware = + TranslationsDialogMiddleware(browserStore = browserStore, sessionId = "tab1") + + val translationStore = TranslationsDialogStore( + initialState = TranslationsDialogState( + initialFrom = Language("en", "English"), + initialTo = Language("fr", "France"), + ), + middlewares = listOf(translationsDialogMiddleware), + ) + translationStore.dispatch(TranslationsDialogAction.TranslateAction).joinBlocking() + + translationStore.waitUntilIdle() + + verify { + browserStore.dispatch( + TranslationsAction.TranslateAction( + tabId = "tab1", + fromLanguage = "en", + toLanguage = "fr", + options = null, + ), + ) + } + } + + @Test + fun `GIVEN translationState WHEN RestoreTranslation from TranslationDialogStore is called THEN call TranslateRestoreAction from BrowserStore`() = + runTest { + val browserStore = mockk(relaxed = true) + val translationsDialogMiddleware = + TranslationsDialogMiddleware(browserStore = browserStore, sessionId = "tab1") + + val translationStore = TranslationsDialogStore( + initialState = TranslationsDialogState(), + middlewares = listOf(translationsDialogMiddleware), + ) + translationStore.dispatch(TranslationsDialogAction.RestoreTranslation).joinBlocking() + + translationStore.waitUntilIdle() + + verify { + browserStore.dispatch( + TranslationsAction.TranslateRestoreAction( + tabId = "tab1", + ), + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogReducerTest.kt b/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogReducerTest.kt new file mode 100644 index 000000000..0aa43d4d2 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogReducerTest.kt @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.translations + +import mozilla.components.concept.engine.translate.Language +import mozilla.components.concept.engine.translate.TranslationError +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class TranslationsDialogReducerTest { + + @Test + fun `WHEN the reducer is called for UpdateFromSelectedLanguage THEN a new state with updated fromSelectedLanguage is returned`() { + val spanishLanguage = Language("es", "Spanish") + val englishLanguage = Language("en", "English") + + val translationsDialogState = TranslationsDialogState(initialTo = spanishLanguage) + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateFromSelectedLanguage(englishLanguage), + ) + + assertEquals(englishLanguage, updatedState.initialFrom) + assertEquals(PositiveButtonType.Enabled, updatedState.positiveButtonType) + + val updatedStateTwo = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateFromSelectedLanguage(spanishLanguage), + ) + + assertEquals(PositiveButtonType.Disabled, updatedStateTwo.positiveButtonType) + } + + @Test + fun `WHEN the reducer is called for UpdateToSelectedLanguage THEN a new state with updated toSelectedLanguage is returned`() { + val spanishLanguage = Language("es", "Spanish") + val englishLanguage = Language("en", "English") + + val translationsDialogState = TranslationsDialogState(initialFrom = spanishLanguage) + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateToSelectedLanguage(englishLanguage), + ) + + assertEquals(englishLanguage, updatedState.initialTo) + assertEquals(PositiveButtonType.Enabled, updatedState.positiveButtonType) + + val updatedStateTwo = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateToSelectedLanguage(spanishLanguage), + ) + + assertEquals(PositiveButtonType.Disabled, updatedStateTwo.positiveButtonType) + } + + @Test + fun `WHEN the reducer is called for UpdateTranslateToLanguages THEN a new state with updated translateToLanguages is returned`() { + val spanishLanguage = Language("es", "Spanish") + val englishLanguage = Language("en", "English") + + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslateToLanguages( + listOf( + spanishLanguage, + englishLanguage, + ), + ), + ) + + assertEquals(listOf(spanishLanguage, englishLanguage), updatedState.toLanguages) + } + + @Test + fun `WHEN the reducer is called for UpdateTranslateFromLanguages THEN a new state with updated translatefromLanguages is returned`() { + val spanishLanguage = Language("es", "Spanish") + val englishLanguage = Language("en", "English") + + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslateFromLanguages( + listOf( + spanishLanguage, + englishLanguage, + ), + ), + ) + + assertEquals(listOf(spanishLanguage, englishLanguage), updatedState.fromLanguages) + } + + @Test + fun `WHEN the reducer is called for DismissDialog THEN a new state with updated dismiss dialog is returned`() { + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.DismissDialog(DismissDialogState.Dismiss), + ) + + assertEquals(DismissDialogState.Dismiss, updatedState.dismissDialogState) + } + + @Test + fun `WHEN the reducer is called for UpdateInProgress THEN a new state with translation in progress is returned`() { + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslationInProgress(true), + ) + + assertEquals(true, updatedState.isTranslationInProgress) + assertEquals(PositiveButtonType.InProgress, updatedState.positiveButtonType) + } + + @Test + fun `WHEN the reducer is called for InitTranslationsDialog THEN a new state for PositiveButtonType is returned`() { + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.InitTranslationsDialog, + ) + + assertEquals(PositiveButtonType.Disabled, updatedState.positiveButtonType) + + val spanishLanguage = Language("es", "Spanish") + val englishLanguage = Language("en", "English") + val translationsDialogStateTwo = TranslationsDialogState( + initialFrom = spanishLanguage, + initialTo = englishLanguage, + positiveButtonType = PositiveButtonType.Enabled, + ) + + val updatedStateTwo = TranslationsDialogReducer.reduce( + translationsDialogStateTwo, + TranslationsDialogAction.InitTranslationsDialog, + ) + + assertEquals(PositiveButtonType.Enabled, updatedStateTwo.positiveButtonType) + } + + @Test + fun `WHEN the reducer is called for UpdateTranslationError THEN a new state with translation error is returned`() { + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslationError( + TranslationError.LanguageNotSupportedError( + null, + ), + ), + ) + + assertTrue(updatedState.error is TranslationError.LanguageNotSupportedError) + assertEquals(PositiveButtonType.Disabled, updatedState.positiveButtonType) + + val updatedStateTwo = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslationError( + TranslationError.CouldNotLoadLanguagesError( + null, + ), + ), + ) + + assertTrue(updatedStateTwo.error is TranslationError.CouldNotLoadLanguagesError) + assertEquals(PositiveButtonType.Enabled, updatedStateTwo.positiveButtonType) + } + + @Test + fun `WHEN the reducer is called for UpdateTranslated THEN a new state with translation translated is returned`() { + val translationsDialogState = TranslationsDialogState() + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslated( + true, + ), + ) + + assertEquals(PositiveButtonType.Disabled, updatedState.positiveButtonType) + assertEquals(true, updatedState.isTranslated) + } + + @Test + fun `WHEN the reducer is called for UpdateTranslatedPageTitle THEN a new state with translation title is returned`() { + val spanishLanguage = Language("es", "Spanish") + val englishLanguage = Language("en", "English") + val translationsDialogState = + TranslationsDialogState(initialTo = englishLanguage, initialFrom = spanishLanguage) + + val updatedState = TranslationsDialogReducer.reduce( + translationsDialogState, + TranslationsDialogAction.UpdateTranslatedPageTitle( + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + spanishLanguage.localizedDisplayName, + englishLanguage.localizedDisplayName, + ), + ), + ) + + assertEquals( + testContext.getString( + R.string.translations_bottom_sheet_title_translation_completed, + spanishLanguage.localizedDisplayName, + englishLanguage.localizedDisplayName, + ), + updatedState.translatedPageTitle, + ) + } +}