Bug 1862289 - Translations UI Integration Low Data Translate

fenix/125.0
iorgamgabriel 3 months ago committed by mergify[bot]
parent 9df7a60315
commit 9656f1f95e

@ -77,12 +77,14 @@ class TranslationsDialogBinding(
// Session Translations State Behavior (Tab)
val sessionTranslationsState = state.sessionState.translationsState
val fromSelected =
sessionTranslationsState.translationEngineState?.initialFromLanguage(
translateFromLanguages,
)
fromSelected?.let {
// Dispatch initialFrom Language only the first time when it is null.
if (fromSelected != null && translationsDialogStore.state.initialFrom == null) {
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateFromSelectedLanguage(
fromSelected,
@ -94,7 +96,9 @@ class TranslationsDialogBinding(
sessionTranslationsState.translationEngineState?.initialToLanguage(
translateToLanguages,
)
toSelected?.let {
// Dispatch initialTo Language only the first time when it is null.
if (toSelected != null && translationsDialogStore.state.initialTo == null) {
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateToSelectedLanguage(
toSelected,
@ -130,6 +134,12 @@ class TranslationsDialogBinding(
TranslationsDialogAction.UpdateTranslationError(sessionTranslationsState.translationError),
)
}
sessionTranslationsState.translationDownloadSize?.let {
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateDownloadTranslationDownloadSize(it),
)
}
}
}
@ -153,11 +163,13 @@ class TranslationsDialogBinding(
),
)
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateTranslated(
true,
),
)
if (!translationsDialogStore.state.isTranslated) {
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateTranslated(
true,
),
)
}
if (translationsDialogStore.state.dismissDialogState == DismissDialogState.WaitingToBeDismissed) {
translationsDialogStore.dispatch(

@ -35,10 +35,14 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLanguageFileDialog
import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLanguageFileDialogType
import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLanguagesFeature
/**
* The enum is to know what bottom sheet to open.
@ -56,8 +60,11 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() {
private val args by navArgs<TranslationsDialogFragmentArgs>()
private val browserStore: BrowserStore by lazy { requireComponents.core.store }
private val translationDialogBinding = ViewBoundFeatureWrapper<TranslationsDialogBinding>()
private val downloadLanguagesFeature =
ViewBoundFeatureWrapper<DownloadLanguagesFeature>()
private lateinit var translationsDialogStore: TranslationsDialogStore
private var isTranslationInProgress: Boolean? = null
private var isDataSaverEnabledAndWifiDisabled = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
super.onCreateDialog(savedInstanceState).apply {
@ -192,9 +199,22 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() {
view = view,
)
translationsDialogStore.dispatch(TranslationsDialogAction.InitTranslationsDialog)
downloadLanguagesFeature.set(
feature = DownloadLanguagesFeature(
context = requireContext(),
wifiConnectionMonitor = requireContext().components.wifiConnectionMonitor,
onDataSaverAndWifiChanged = {
isDataSaverEnabledAndWifiDisabled = it
},
),
owner = this,
view = view,
)
}
@Composable
@Suppress("LongMethod")
private fun TranslationsDialogContent(learnMoreUrl: String, onSettingClicked: () -> Unit) {
val translationsDialogState =
translationsDialogStore.observeAsComposableState { it }.value
@ -206,6 +226,10 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() {
dismissDialog()
}
var showDownloadLanguageFileDialog by remember {
mutableStateOf(false)
}
TranslationsDialog(
translationsDialogState = translationsDialogState,
learnMoreUrl = learnMoreUrl,
@ -216,7 +240,15 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() {
if (state.error is TranslationError.CouldNotLoadLanguagesError) {
translationsDialogStore.dispatch(TranslationsDialogAction.FetchSupportedLanguages)
} else {
translationsDialogStore.dispatch(TranslationsDialogAction.TranslateAction)
if (
isDataSaverEnabledAndWifiDisabled &&
!requireContext().settings().ignoreTranslationsDataSaverWarning &&
state.translationDownloadSize != null
) {
showDownloadLanguageFileDialog = true
} else {
translationsDialogStore.dispatch(TranslationsDialogAction.TranslateAction)
}
}
},
onNegativeButtonClicked = {
@ -225,21 +257,58 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() {
}
dismiss()
},
onFromSelected = {
onFromSelected = { fromLanguage ->
state.initialTo?.let {
translationsDialogStore.dispatch(
TranslationsDialogAction.FetchDownloadFileSizeAction(
toLanguage = it,
fromLanguage = fromLanguage,
),
)
}
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateFromSelectedLanguage(
it,
fromLanguage,
),
)
},
onToSelected = {
onToSelected = { toLanguage ->
state.initialFrom?.let {
translationsDialogStore.dispatch(
TranslationsDialogAction.FetchDownloadFileSizeAction(
toLanguage = toLanguage,
fromLanguage = it,
),
)
}
translationsDialogStore.dispatch(
TranslationsDialogAction.UpdateToSelectedLanguage(
it,
toLanguage,
),
)
},
)
var checkBoxEnabled by remember { mutableStateOf(false) }
if (showDownloadLanguageFileDialog) {
state.translationDownloadSize?.size?.let { fileSize ->
DownloadLanguageFileDialog(
downloadLanguageDialogType = DownloadLanguageFileDialogType.TranslationRequest,
fileSize = fileSize,
isCheckBoxEnabled = checkBoxEnabled,
onSavingModeStateChange = { checkBoxEnabled = it },
onConfirmDownload = {
requireContext().settings().ignoreTranslationsDataSaverWarning =
checkBoxEnabled
showDownloadLanguageFileDialog = false
translationsDialogStore.dispatch(TranslationsDialogAction.TranslateAction)
},
onCancel = { showDownloadLanguageFileDialog = false },
)
}
}
}
}

@ -24,6 +24,16 @@ class TranslationsDialogMiddleware(
action: TranslationsDialogAction,
) {
when (action) {
is TranslationsDialogAction.FetchDownloadFileSizeAction -> {
browserStore.dispatch(
TranslationsAction.FetchTranslationDownloadSizeAction(
tabId = sessionId,
fromLanguage = action.fromLanguage,
toLanguage = action.toLanguage,
),
)
}
is TranslationsDialogAction.FetchSupportedLanguages -> {
browserStore.dispatch(
TranslationsAction.OperationRequestedAction(

@ -6,6 +6,7 @@ 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.TranslationDownloadSize
import mozilla.components.concept.engine.translate.TranslationError
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
@ -33,6 +34,8 @@ class TranslationsDialogStore(
* @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 translationDownloadSize A data class to contain information
* related to the download size required for a given translation to/from pair.
* @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.
@ -45,6 +48,7 @@ data class TranslationsDialogState(
var isTranslated: Boolean = false,
val isTranslationInProgress: Boolean = false,
val positiveButtonType: PositiveButtonType? = null,
val translationDownloadSize: TranslationDownloadSize? = null,
val error: TranslationError? = null,
val dismissDialogState: DismissDialogState? = null,
val initialFrom: Language? = null,
@ -139,6 +143,18 @@ sealed class TranslationsDialogAction : Action {
* Updates the dialog title if the page was translated.
*/
data class UpdateTranslatedPageTitle(val title: String) : TranslationsDialogAction()
/**
* Updates the translation download file size.
*/
data class UpdateDownloadTranslationDownloadSize(val translationDownloadSize: TranslationDownloadSize? = null) :
TranslationsDialogAction()
/**
* Fetch the translation download file size.
*/
data class FetchDownloadFileSizeAction(val toLanguage: Language, val fromLanguage: Language) :
TranslationsDialogAction()
}
/**
@ -248,9 +264,7 @@ internal object TranslationsDialogReducer {
is TranslationsDialogAction.UpdateTranslationError -> {
state.copy(
error = action.translationError,
positiveButtonType = if (
action.translationError is TranslationError.LanguageNotSupportedError
) {
positiveButtonType = if (action.translationError is TranslationError.LanguageNotSupportedError) {
PositiveButtonType.Disabled
} else {
PositiveButtonType.Enabled
@ -269,13 +283,32 @@ internal object TranslationsDialogReducer {
state.copy(translatedPageTitle = action.title)
}
is TranslationsDialogAction.UpdateDownloadTranslationDownloadSize -> {
state.copy(
translationDownloadSize = if (
action.translationDownloadSize?.fromLanguage == state.initialFrom &&
action.translationDownloadSize?.toLanguage == state.initialTo &&
isTranslationDownloadSizeValid(action.translationDownloadSize)
) {
action.translationDownloadSize
} else {
null
},
)
}
is TranslationsDialogAction.TranslateAction,
TranslationsDialogAction.FetchSupportedLanguages,
TranslationsDialogAction.RestoreTranslation,
is TranslationsDialogAction.FetchDownloadFileSizeAction,
-> {
// handled by [TranslationsDialogMiddleware]
state
}
}
}
private fun isTranslationDownloadSizeValid(translationDownloadSize: TranslationDownloadSize?) =
translationDownloadSize?.size != 0L &&
translationDownloadSize?.error == null
}

@ -36,19 +36,19 @@ import org.mozilla.fenix.theme.FirefoxTheme
/**
* Download Languages File Dialog.
*
* @param downloadLanguageDialogType Whether the download language file item is
* of type all languages,single file translation request or default.
* @param fileSize Language file size in bytes that should be displayed in the dialogue title.
* @param isCheckBoxEnabled Whether saving mode checkbox is checked or unchecked.
* @param isAllLanguagesItemType Whether the download language file item is of type all languages.
* @param onSavingModeStateChange Invoked when the user clicks on the checkbox of the saving mode state.
* @param onConfirmDownload Invoked when the user click on the "Download" dialog button.
* @param onCancel Invoked when the user clicks on the "Cancel" dialog button.
*/
@Composable
fun DownloadLanguageFileDialog(
downloadLanguageDialogType: DownloadLanguageFileDialogType,
fileSize: Long,
isCheckBoxEnabled: Boolean,
isAllLanguagesItemType: Boolean,
onSavingModeStateChange: (Boolean) -> Unit,
onConfirmDownload: () -> Unit,
onCancel: () -> Unit,
@ -62,18 +62,29 @@ fun DownloadLanguageFileDialog(
)
.padding(16.dp),
) {
val title =
if (downloadLanguageDialogType is DownloadLanguageFileDialogType.TranslationRequest) {
stringResource(
R.string.translations_download_language_file_dialog_title,
fileSize.toMegabyteOrKilobyteString(),
)
} else {
stringResource(
R.string.download_language_file_dialog_title,
fileSize.toMegabyteOrKilobyteString(),
)
}
Text(
text = stringResource(
R.string.download_language_file_dialog_title,
fileSize.toMegabyteOrKilobyteString(),
),
text = title,
modifier = Modifier
.semantics { heading() },
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline7,
)
if (isAllLanguagesItemType) {
if (downloadLanguageDialogType is DownloadLanguageFileDialogType.AllLanguages ||
downloadLanguageDialogType is DownloadLanguageFileDialogType.TranslationRequest
) {
Text(
text = stringResource(
R.string.download_language_file_dialog_message_all_languages,
@ -89,11 +100,14 @@ fun DownloadLanguageFileDialog(
onSavingModeStateChange = onSavingModeStateChange,
)
val primaryButtonText: String = if (isAllLanguagesItemType) {
stringResource(id = R.string.download_language_file_dialog_positive_button_text_all_languages)
} else {
stringResource(id = R.string.download_language_file_dialog_positive_button_text)
}
val primaryButtonText: String =
if (downloadLanguageDialogType is DownloadLanguageFileDialogType.AllLanguages ||
downloadLanguageDialogType is DownloadLanguageFileDialogType.TranslationRequest
) {
stringResource(id = R.string.download_language_file_dialog_positive_button_text_all_languages)
} else {
stringResource(id = R.string.download_language_file_dialog_positive_button_text)
}
PrimaryButton(
text = primaryButtonText,
@ -163,9 +177,9 @@ private fun DownloadLanguageFileDialogCheckbox(
private fun PrefDownloadLanguageFileDialogPreviewAllLanguages() {
FirefoxTheme {
DownloadLanguageFileDialog(
downloadLanguageDialogType = DownloadLanguageFileDialogType.AllLanguages,
fileSize = 4000L,
isCheckBoxEnabled = true,
isAllLanguagesItemType = true,
onSavingModeStateChange = {},
onConfirmDownload = {},
onCancel = {},
@ -173,14 +187,37 @@ private fun PrefDownloadLanguageFileDialogPreviewAllLanguages() {
}
}
/**
* Download Languages File Dialog Type.
*/
sealed class DownloadLanguageFileDialogType {
/**
* All language files need to be downloaded.
*/
data object AllLanguages : DownloadLanguageFileDialogType()
/**
* Only one language package needs to be downloaded.
*/
data object Default : DownloadLanguageFileDialogType()
/**
* When the user presses the translate button, the site needs to be translated.
* To perform this translation, the device will need to download a language model to perform
* this specific translation, if not already downloaded.
*/
data object TranslationRequest : DownloadLanguageFileDialogType()
}
@Composable
@LightDarkPreview
private fun PrefDownloadLanguageFileDialogPreview() {
FirefoxTheme {
DownloadLanguageFileDialog(
downloadLanguageDialogType = DownloadLanguageFileDialogType.Default,
fileSize = 4000L,
isCheckBoxEnabled = false,
isAllLanguagesItemType = false,
onSavingModeStateChange = {},
onConfirmDownload = {},
onCancel = {},

@ -74,10 +74,15 @@ class LanguageDialogPreferenceFragment : DialogFragment() {
FirefoxTheme {
var checkBoxEnabled by remember { mutableStateOf(false) }
DownloadLanguageFileDialog(
downloadLanguageDialogType = if (args.downloadLanguageItemStatePreference.type ==
DownloadLanguageItemTypePreference.AllLanguages
) {
DownloadLanguageFileDialogType.AllLanguages
} else {
DownloadLanguageFileDialogType.Default
},
fileSize = args.itemFileSizePreference,
isCheckBoxEnabled = checkBoxEnabled,
isAllLanguagesItemType = args.downloadLanguageItemStatePreference.type ==
DownloadLanguageItemTypePreference.AllLanguages,
onSavingModeStateChange = { checkBoxEnabled = it },
onConfirmDownload = {
requireContext().settings().ignoreTranslationsDataSaverWarning =

@ -2377,6 +2377,12 @@
<string name="translation_error_language_not_supported_learn_more">Learn more</string>
<!-- Snackbar title shown if the user closes the Translation Request dialogue and a translation is in progress. -->
<string name="translation_in_progress_snackbar">Translating…</string>
<!-- Title for the data saving mode warning dialog used in the translation request dialog.
This dialog will be presented when the user attempts to perform
a translation without the necessary language files downloaded first when Android's data saver mode is enabled and the user is not using WiFi.
The first parameter is the size in kilobytes or megabytes of the language file. -->
<string name="translations_download_language_file_dialog_title">Download language in data saving mode (%1$s)?</string>
<!-- Translations options dialog -->
<!-- Title of the translation options dialog that allows a user to set their translation options for the site the user is currently on. -->

@ -11,6 +11,7 @@ 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.TranslationDownloadSize
import mozilla.components.concept.engine.translate.TranslationEngineState
import mozilla.components.concept.engine.translate.TranslationError
import mozilla.components.concept.engine.translate.TranslationOperation
@ -303,4 +304,49 @@ class TranslationsDialogBindingTest {
TranslationsDialogAction.UpdateTranslationError(fetchError),
)
}
@Test
fun `WHEN set translation download size action 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 translationDownloadSize = TranslationDownloadSize(
fromLanguage = fromLanguage,
toLanguage = toLanguage,
size = 1000L,
)
browserStore.dispatch(
TranslationsAction.SetTranslationDownloadSizeAction(
tabId = tab.id,
translationSize = translationDownloadSize,
),
).joinBlocking()
verify(translationsDialogStore).dispatch(
TranslationsDialogAction.UpdateDownloadTranslationDownloadSize(translationDownloadSize),
)
}
}

@ -98,4 +98,35 @@ class TranslationsDialogMiddlewareTest {
)
}
}
@Test
fun `GIVEN translationState WHEN FetchDownloadFileSizeAction from TranslationDialogStore is called THEN call FetchTranslationDownloadSizeAction from BrowserStore`() =
runTest {
val browserStore = mockk<BrowserStore>(relaxed = true)
val translationsDialogMiddleware =
TranslationsDialogMiddleware(browserStore = browserStore, sessionId = "tab1")
val translationStore = TranslationsDialogStore(
initialState = TranslationsDialogState(),
middlewares = listOf(translationsDialogMiddleware),
)
translationStore.dispatch(
TranslationsDialogAction.FetchDownloadFileSizeAction(
toLanguage = Language("en", "English"),
fromLanguage = Language("fr", "France"),
),
).joinBlocking()
translationStore.waitUntilIdle()
verify {
browserStore.dispatch(
TranslationsAction.FetchTranslationDownloadSizeAction(
tabId = "tab1",
fromLanguage = Language("fr", "France"),
toLanguage = Language("en", "English"),
),
)
}
}
}

@ -5,6 +5,7 @@
package org.mozilla.fenix.translations
import mozilla.components.concept.engine.translate.Language
import mozilla.components.concept.engine.translate.TranslationDownloadSize
import mozilla.components.concept.engine.translate.TranslationError
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
@ -226,4 +227,52 @@ class TranslationsDialogReducerTest {
updatedState.translatedPageTitle,
)
}
@Test
fun `WHEN the reducer is called for UpdateDownloadTranslationDownloadSize THEN a new state with translationDownloadSize is returned`() {
val spanishLanguage = Language("es", "Spanish")
val englishLanguage = Language("en", "English")
val translationsDialogState =
TranslationsDialogState(initialTo = englishLanguage, initialFrom = spanishLanguage)
val translationDownloadSize = TranslationDownloadSize(
fromLanguage = spanishLanguage,
toLanguage = englishLanguage,
size = 1000L,
)
val updatedState = TranslationsDialogReducer.reduce(
translationsDialogState,
TranslationsDialogAction.UpdateDownloadTranslationDownloadSize(
translationDownloadSize,
),
)
assertEquals(
translationDownloadSize,
updatedState.translationDownloadSize,
)
}
@Test
fun `WHEN the reducer is called for UpdateDownloadTranslationDownloadSize with a invalid object THEN a new state with translationDownloadSize null is returned`() {
val spanishLanguage = Language("es", "Spanish")
val englishLanguage = Language("en", "English")
val translationsDialogState =
TranslationsDialogState(initialTo = englishLanguage, initialFrom = spanishLanguage)
val translationDownloadSize = TranslationDownloadSize(
fromLanguage = englishLanguage,
toLanguage = spanishLanguage,
size = 0L,
)
val updatedState = TranslationsDialogReducer.reduce(
translationsDialogState,
TranslationsDialogAction.UpdateDownloadTranslationDownloadSize(
translationDownloadSize,
),
)
assertEquals(
null,
updatedState.translationDownloadSize,
)
}
}

Loading…
Cancel
Save