TranslationsMiddleware.kt
/* 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 mozilla.components.browser.state.engine.middleware
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.InitAction
import mozilla.components.browser.state.action.LocaleAction
import mozilla.components.browser.state.action.TranslationsAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.translate.Language
import mozilla.components.concept.engine.translate.LanguageModel
import mozilla.components.concept.engine.translate.LanguageModel.Companion.areModelsProcessing
import mozilla.components.concept.engine.translate.LanguageSetting
import mozilla.components.concept.engine.translate.ModelManagementOptions
import mozilla.components.concept.engine.translate.ModelOperation
import mozilla.components.concept.engine.translate.ModelState
import mozilla.components.concept.engine.translate.TranslationDownloadSize
import mozilla.components.concept.engine.translate.TranslationError
import mozilla.components.concept.engine.translate.TranslationOperation
import mozilla.components.concept.engine.translate.TranslationPageSettingOperation
import mozilla.components.concept.engine.translate.TranslationPageSettings
import mozilla.components.concept.engine.translate.findLanguage
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.support.base.log.logger.Logger
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* This middleware is for use with managing any states or resources required for translating a
* webpage.
*/
@Suppress("LargeClass")
class TranslationsMiddleware(
private val engine: Engine,
private val scope: CoroutineScope,
) : Middleware<BrowserState, BrowserAction> {
private val logger = Logger("TranslationsMiddleware")
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
// Pre process actions
when (action) {
is InitAction ->
context.store.dispatch(TranslationsAction.InitTranslationsBrowserState)
is LocaleAction.UpdateLocaleAction -> {
logger.info("Detected app locale change.")
scope.launch {
// This information is dependent on the app language
requestSupportedLanguages(context, null)
requestLanguageModels(context, null)
}
}
is TranslationsAction.InitTranslationsBrowserState -> {
scope.launch {
val engineIsSupported = requestEngineSupport(context)
if (engineIsSupported == true) {
initializeBrowserStore(context)
}
}
}
is TranslationsAction.TranslateExpectedAction -> {
requestDefaultModelDownloadSize(context, action.tabId)
}
is TranslationsAction.OperationRequestedAction -> {
when (action.operation) {
TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> {
scope.launch {
requestSupportedLanguages(context, action.tabId)
}
}
TranslationOperation.FETCH_LANGUAGE_MODELS -> {
scope.launch {
requestLanguageModels(context, action.tabId)
}
}
TranslationOperation.FETCH_PAGE_SETTINGS -> {
val tabId = action.tabId ?: context.state.selectedTab?.id
if (action.tabId == null) {
logger.warn(
"Passed null tabId to FETCH_PAGE_SETTINGS, " +
"Will use current selected tab.",
)
}
if (tabId != null) {
scope.launch {
context.state.selectedTab?.let {
requestPageSettings(context, it.id)
}
}
} else {
logger.warn(
"Passed null tabId to FETCH_PAGE_SETTINGS, " +
"and no selected tab was available. Performing no action.",
)
}
}
TranslationOperation.FETCH_OFFER_SETTING -> {
scope.launch {
requestOfferSetting(context, action.tabId)
}
}
TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS -> {
scope.launch {
requestLanguageSettings(context, action.tabId)
}
}
TranslationOperation.FETCH_NEVER_TRANSLATE_SITES -> {
scope.launch {
requestNeverTranslateSites(context, action.tabId)
}
}
TranslationOperation.TRANSLATE,
TranslationOperation.RESTORE,
-> Unit
}
}
is TranslationsAction.FetchTranslationDownloadSizeAction -> {
scope.launch {
requestTranslationSize(
context = context,
tabId = action.tabId,
fromLanguage = action.fromLanguage,
toLanguage = action.toLanguage,
)
}
}
is TranslationsAction.RemoveNeverTranslateSiteAction -> {
scope.launch {
removeNeverTranslateSite(context, action.origin)
}
}
is TranslationsAction.UpdatePageSettingAction -> {
when (action.operation) {
TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP ->
scope.launch {
updateAlwaysOfferPopupPageSetting(
setting = action.setting,
)
}
TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE ->
scope.launch {
updateLanguagePageSetting(
context = context,
tabId = action.tabId,
setting = action.setting,
settingType = LanguageSetting.ALWAYS,
)
}
TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE ->
scope.launch {
updateLanguagePageSetting(
context = context,
tabId = action.tabId,
setting = action.setting,
settingType = LanguageSetting.NEVER,
)
}
TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_SITE ->
scope.launch {
updateNeverTranslateSitePageSetting(
context = context,
tabId = action.tabId,
setting = action.setting,
)
}
}
}
is TranslationsAction.UpdateGlobalOfferTranslateSettingAction -> {
scope.launch {
updateAlwaysOfferPopupPageSetting(
setting = action.offerTranslation,
)
}
}
is TranslationsAction.UpdateLanguageSettingsAction -> {
scope.launch {
updateLanguageSetting(
context = context,
languageCode = action.languageCode,
setting = action.setting,
)
}
}
is TranslationsAction.ManageLanguageModelsAction -> {
scope.launch {
updateLanguageModel(
context = context,
options = action.options,
)
}
}
else -> {
// no-op
}
}
// Continue to post process actions
next(action)
}
/**
* Use this to initialize translations data for [BrowserState.translationEngine]. If an
* issue occurs, the relevant error will be set on [BrowserState.translationEngine].
*
* This will populate:
* Language Support - [requestSupportedLanguages]
* Language Models - [requestLanguageModels]
* Language Settings - [requestLanguageSettings]
* Never Translate Sites List - [requestNeverTranslateSites]
* Offer Setting - [requestOfferSetting]
*
* @param context Context to use to dispatch to the store.
*/
private fun initializeBrowserStore(
context: MiddlewareContext<BrowserState, BrowserAction>,
) {
requestSupportedLanguages(context)
requestLanguageModels(context)
requestLanguageSettings(context)
requestNeverTranslateSites(context)
requestOfferSetting(context)
}
/**
* Checks if the translations engine supports the device architecture and updates the state on
* [BrowserState.translationEngine].
*
* @param context Context to use to dispatch to the store.
* @return Whether the engine is supported or not, or null when the support cannot be
* determined.
*/
private suspend fun requestEngineSupport(
context: MiddlewareContext<BrowserState, BrowserAction>,
): Boolean? {
return suspendCoroutine { continuation ->
engine.isTranslationsEngineSupported(
onSuccess = { isEngineSupported ->
context.store.dispatch(
TranslationsAction.SetEngineSupportedAction(
isEngineSupported = isEngineSupported,
),
)
logger.info("Success requesting engine support. isEngineSupported: $isEngineSupported")
continuation.resume(isEngineSupported)
},
onError = { error ->
context.store.dispatch(
TranslationsAction.EngineExceptionAction(
error = TranslationError.UnknownEngineSupportError(error),
),
)
logger.error("Error requesting engine support: ", error)
continuation.resume(null)
},
)
}
}
/**
* Retrieves the list of supported languages and dispatches the result to the
* [BrowserState.translationEngine] via [TranslationsAction.SetSupportedLanguagesAction] or
* else dispatches the failure.
*
* For failure dispatching:
* If a tab ID is not provided, then only [TranslationsAction.EngineExceptionAction] will be
* dispatched to set the error on the [BrowserState.translationEngine].
*
* If a tab ID is provided, then [TranslationsAction.EngineExceptionAction]
* AND [TranslationsAction.TranslateExceptionAction] will be dispatched
* to set the error both on the [BrowserState.translationEngine] and
* [SessionState.translationsState].
*
* @param context Context to use to dispatch to the store.
* @param tabId If a Tab ID associated with the request for error handling.
* If null, this will only dispatch errors on the global translations browser state.
*
*/
private fun requestSupportedLanguages(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String? = null,
) {
engine.getSupportedTranslationLanguages(
onSuccess = {
context.store.dispatch(
TranslationsAction.SetSupportedLanguagesAction(
supportedLanguages = it,
),
)
// Ensures error is cleared, if a tab made this request.
if (tabId != null) {
context.store.dispatch(
TranslationsAction.TranslateSuccessAction(
tabId = tabId,
operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
),
)
}
logger.info("Success requesting supported languages.")
},
onError = {
context.store.dispatch(
TranslationsAction.EngineExceptionAction(
error = TranslationError.CouldNotLoadLanguagesError(it),
),
)
if (tabId != null) {
context.store.dispatch(
TranslationsAction.TranslateExceptionAction(
tabId = tabId,
operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
translationError = TranslationError.CouldNotLoadLanguagesError(it),
),
)
}
logger.error("Error requesting supported languages: ", it)
},
)
}
/**
* Retrieves the list of language machine learning translation models the translation engine
* has available and dispatches the result to the [BrowserState.translationEngine]
* via [TranslationsAction.SetLanguageModelsAction] or else dispatches the failure.
*
* For failure dispatching:
* If a tab ID is not provided, then only [TranslationsAction.EngineExceptionAction] will be
* dispatched to set the error on the [BrowserState.translationEngine].
*
* If a tab ID is provided, then [TranslationsAction.EngineExceptionAction]
* AND [TranslationsAction.TranslateExceptionAction] will be dispatched
* to set the error both on the [BrowserState.translationEngine] and
* [SessionState.translationsState].
*
* @param context Context to use to dispatch to the store.
* @param tabId If a Tab ID associated with the request for error handling.
* If null, this will only dispatch errors on the global translations browser state.
*
*/
private fun requestLanguageModels(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String? = null,
) {
engine.getTranslationsModelDownloadStates(
onSuccess = {
context.store.dispatch(
TranslationsAction.SetLanguageModelsAction(
languageModels = it,
),
)
logger.info("Success requesting language models.")
},
onError = { error ->
context.store.dispatch(
TranslationsAction.EngineExceptionAction(
error = TranslationError.ModelCouldNotRetrieveError(error),
),
)
if (tabId != null) {
context.store.dispatch(
TranslationsAction.TranslateExceptionAction(
tabId = tabId,
operation = TranslationOperation.FETCH_LANGUAGE_MODELS,
translationError = TranslationError.ModelCouldNotRetrieveError(error),
),
)
}
logger.error("Error requesting language models: ", error)
},
)
}
/**
* Retrieves the list of never translate sites and dispatches the result to the
* store via [TranslationsAction.SetNeverTranslateSitesAction] or else
* dispatches the failure via [TranslationsAction.EngineExceptionAction] and
* when a [tabId] is provided, [TranslationsAction.TranslateExceptionAction].
*
* @param context Context to use to dispatch to the store.
* @param tabId Tab ID associated with the request.
*/
private fun requestNeverTranslateSites(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String? = null,
) {
engine.getNeverTranslateSiteList(
onSuccess = {
context.store.dispatch(
TranslationsAction.SetNeverTranslateSitesAction(
neverTranslateSites = it,
),
)
logger.info("Success requesting never translate sites.")
},
onError = {
context.store.dispatch(
TranslationsAction.EngineExceptionAction(
error = TranslationError.CouldNotLoadNeverTranslateSites(it),
),
)
if (tabId != null) {
context.store.dispatch(
TranslationsAction.TranslateExceptionAction(
tabId = tabId,
operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
translationError = TranslationError.CouldNotLoadNeverTranslateSites(it),
),
)
}
logger.error("Error requesting never translate sites: ", it)
},
)
}
/**
* Removes the site from the list of never translate sites using [scope] and dispatches the result to the
* store via [TranslationsAction.SetNeverTranslateSitesAction] or else dispatches the failure
* [TranslationsAction.TranslateExceptionAction].
*
* @param context Context to use to dispatch to the store.
* @param origin A site origin URI that will have the specified never translate permission set.
*/
private fun removeNeverTranslateSite(
context: MiddlewareContext<BrowserState, BrowserAction>,
origin: String,
) {
engine.setNeverTranslateSpecifiedSite(
origin = origin,
setting = false,
onSuccess = {
logger.info("Success changing never translate sites.")
},
onError = {
logger.error("Error removing site from never translate list: ", it)
// Fetch never translate sites to ensure the state matches the engine, because it
// was proactively removed in the reducer.
requestNeverTranslateSites(context)
},
)
}
/**
* Retrieves the page settings and dispatches the result to the
* store via [TranslationsAction.SetPageSettingsAction] or else dispatches the failure
* [TranslationsAction.TranslateExceptionAction].
*
* @param context Context to use to dispatch to the store.
* @param tabId Tab ID associated with the request.
*/
private suspend fun requestPageSettings(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
) {
logger.info("Requesting page settings.")
// Always offer setting
val alwaysOfferPopup: Boolean = engine.getTranslationsOfferPopup()
// Page language settings
val pageLanguage = context.store.state.findTab(tabId)
?.translationsState?.translationEngineState?.detectedLanguages?.documentLangTag
val setting = pageLanguage?.let { getLanguageSetting(it) }
val alwaysTranslateLanguage = setting?.toBoolean(LanguageSetting.ALWAYS)
val neverTranslateLanguage = setting?.toBoolean(LanguageSetting.NEVER)
// Never translate site
val engineSession = context.store.state.findTab(tabId)
?.engineState?.engineSession
val neverTranslateSite = engineSession?.let { getNeverTranslateSiteSetting(it) }
if (
alwaysTranslateLanguage != null &&
neverTranslateLanguage != null &&
neverTranslateSite != null
) {
logger.info("Successfully found all page settings.")
context.store.dispatch(
TranslationsAction.SetPageSettingsAction(
tabId = tabId,
pageSettings = TranslationPageSettings(
alwaysOfferPopup = alwaysOfferPopup,
alwaysTranslateLanguage = alwaysTranslateLanguage,
neverTranslateLanguage = neverTranslateLanguage,
neverTranslateSite = neverTranslateSite,
),
),
)
} else {
logger.error("Could not find all page settings.")
// Any null values indicate something went wrong, alert an error occurred
context.store.dispatch(
TranslationsAction.TranslateExceptionAction(
tabId = tabId,
operation = TranslationOperation.FETCH_PAGE_SETTINGS,
translationError = TranslationError.CouldNotLoadPageSettingsError(null),
),
)
}
}
/**
* Retrieves the setting to always offer to translate and dispatches the result to the
* store via [TranslationsAction.SetGlobalOfferTranslateSettingAction]. Will additionally
* dispatch a request to update page settings, when a [tabId] is provided.
*
* @param context Context to use to dispatch to the store.
* @param tabId Tab ID associated with the request.
*/
private fun requestOfferSetting(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String? = null,
) {
logger.info("Requesting offer setting.")
val alwaysOfferPopup: Boolean = engine.getTranslationsOfferPopup()
context.store.dispatch(
TranslationsAction.SetGlobalOfferTranslateSettingAction(
offerTranslation = alwaysOfferPopup,
),
)
if (tabId != null) {
// Fetch page settings to ensure the state matches the engine.
context.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = tabId,
operation = TranslationOperation.FETCH_PAGE_SETTINGS,
),
)
}
}
/**
* Fetches the always or never language setting synchronously from the engine. Will
* return null if an error occurs.
*
* @param pageLanguage Page language to check the translation preferences for.
* @return The page translate language setting or null.
*/
private suspend fun getLanguageSetting(pageLanguage: String): LanguageSetting? {
return suspendCoroutine { continuation ->
engine.getLanguageSetting(
languageCode = pageLanguage,
onSuccess = { setting ->
logger.info("Success requesting language settings.")
continuation.resume(setting)
},
onError = {
logger.error("Could not retrieve language settings: $it")
continuation.resume(null)
},
)
}
}
/**
* Retrieves the list of languages and their settings and dispatches the result to the
* [BrowserState.translationEngine] via [TranslationsAction.SetLanguageSettingsAction] or
* else dispatches the failure.
*
* For failure dispatching:
* If a tab ID is not provided, then only [TranslationsAction.EngineExceptionAction] will be
* dispatched to set the error on the [BrowserState.translationEngine].
*
* If a tab ID is provided, then [TranslationsAction.EngineExceptionAction]
* AND [TranslationsAction.TranslateExceptionAction] will be dispatched
* to set the error both on the [BrowserState.translationEngine] and
* [SessionState.translationsState].
*
* @param context Context to use to dispatch to the store.
* @param tabId If a Tab ID is associated with the request for error handling.
* If null, this will only dispatch errors on the global translations browser state.
*/
private fun requestLanguageSettings(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String? = null,
) {
engine.getLanguageSettings(
onSuccess = { settings ->
context.store.dispatch(
TranslationsAction.SetLanguageSettingsAction(
languageSettings = settings,
),
)
logger.info("Success requesting language settings.")
},
onError = {
context.store.dispatch(
TranslationsAction.EngineExceptionAction(
error = TranslationError.CouldNotLoadLanguageSettingsError(it),
),
)
if (tabId != null) {
context.store.dispatch(
TranslationsAction.TranslateExceptionAction(
tabId = tabId,
operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
translationError = TranslationError.CouldNotLoadLanguageSettingsError(it),
),
)
}
logger.error("Error requesting language settings: ", it)
},
)
}
/**
* Fetches the never translate site setting synchronously from the [EngineSession]. Will
* return null if an error occurs.
*
* @param engineSession With page context on how to complete this operation.
* @return The never translate site setting from the [EngineSession] or null.
*/
private suspend fun getNeverTranslateSiteSetting(engineSession: EngineSession): Boolean? {
return suspendCoroutine { continuation ->
engineSession.getNeverTranslateSiteSetting(
onResult = { setting ->
logger.info("Success requesting never translate site settings.")
continuation.resume(setting)
},
onException = {
logger.error("Could not retrieve never translate site settings: $it")
continuation.resume(null)
},
)
}
}
/**
* Retrieves the download size and dispatches the result to the
* [SessionState.translationsState] on the [BrowserStore]
* via [TranslationsAction.SetTranslationDownloadSizeAction].
*
* @param context Context to use to dispatch to the store.
* @param tabId Tab ID associated with the request.
* @param fromLanguage The from language to request the translation download size for.
* @param toLanguage The to language to request the translation download size for.
*/
private fun requestTranslationSize(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
fromLanguage: Language,
toLanguage: Language,
) {
engine.getTranslationsPairDownloadSize(
fromLanguage = fromLanguage.code,
toLanguage = toLanguage.code,
onSuccess = { size ->
context.store.dispatch(
TranslationsAction.SetTranslationDownloadSizeAction(
tabId = tabId,
translationSize = TranslationDownloadSize(
fromLanguage = fromLanguage,
toLanguage = toLanguage,
size = size,
error = null,
),
),
)
logger.info("Success requesting download size.")
},
onError = { error ->
context.store.dispatch(
TranslationsAction.SetTranslationDownloadSizeAction(
tabId = tabId,
translationSize = TranslationDownloadSize(
fromLanguage = fromLanguage,
toLanguage = toLanguage,
size = null,
error = TranslationError.CouldNotDetermineDownloadSizeError(null),
),
),
)
logger.error("Error requesting download size: ", error)
},
)
}
/**
* Fetches the expected translation model download size assuming the user intends to complete
* a translation using the detected default `from` (page language) and `to` (user preferred)
* languages.
*
* If the detected default languages are available, then this will fetch and set the
* corresponding model download size on [SessionState.translationsState].
*
* If no defaults are available, then no action will occur.
*
* @param context Context to use to dispatch to the store.
* @param tabId Tab ID associated with the request.
*/
private fun requestDefaultModelDownloadSize(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
) {
val fromLanguage = getDefaultFromLanguage(context, tabId) ?: return
val toLanguage = getDefaultToLanguage(context, tabId) ?: return
context.store.dispatch(
TranslationsAction.FetchTranslationDownloadSizeAction(
tabId = tabId,
fromLanguage = fromLanguage,
toLanguage = toLanguage,
),
)
}
/**
* Updates the always offer popup setting with the [Engine].
*
* @param setting The value of the always offer setting to update.
*/
private fun updateAlwaysOfferPopupPageSetting(
setting: Boolean,
) {
logger.info("Setting the always offer translations popup preference.")
engine.setTranslationsOfferPopup(setting)
}
/**
* Updates the language settings with the [Engine].
*
* If an error occurs, then the method will request the page settings be re-fetched and set on
* the browser store.
*
* @param context The context used to request the page settings.
* @param tabId Tab ID associated with the request.
* @param setting The value of the always offer setting to update.
* @param settingType If the boolean to update is from the
* [LanguageSetting.ALWAYS] or [LanguageSetting.NEVER] perspective.
*/
private fun updateLanguagePageSetting(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
setting: Boolean,
settingType: LanguageSetting,
) {
logger.info("Preparing to update the translations language preference.")
val pageLanguage = context.store.state.findTab(tabId)
?.translationsState?.translationEngineState?.detectedLanguages?.documentLangTag
val convertedSetting = settingType.toLanguageSetting(setting)
if (pageLanguage == null || convertedSetting == null) {
logger.info("An issue occurred while preparing to update the language setting.")
// Fetch page settings to ensure the state matches the engine.
context.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = tabId,
operation = TranslationOperation.FETCH_PAGE_SETTINGS,
),
)
} else {
logger.info("Updating language setting.")
updateLanguageSetting(context, tabId, pageLanguage, convertedSetting)
}
}
/**
* Updates the language settings with the [Engine].
*
* If an error occurs, and a [tabId] is known then the method will request the page settings be
* re-fetched and set on the browser store.
*
* @param context The context used to request the page settings.
* @param tabId Tab ID associated with the request.
* @param languageCode The BCP-47 language to update.
* @param setting The new language setting for the [languageCode].
*/
private fun updateLanguageSetting(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String? = null,
languageCode: String,
setting: LanguageSetting,
) {
logger.info("Setting the translations language preference.")
engine.setLanguageSetting(
languageCode = languageCode,
languageSetting = setting,
onSuccess = {
// Value was proactively updated in [TranslationsStateReducer] for
// [TranslationsBrowserState.languageSettings]
if (tabId != null) {
// Ensure the session's page settings remain in sync with this update.
context.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = tabId,
operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
),
)
}
logger.info("Successfully updated the language preference.")
},
onError = {
logger.error("Could not update the language preference.", it)
// The browser store [TranslationsBrowserState.languageSettings] is out of sync,
// re-request to sync the state.
requestLanguageSettings(context, tabId)
if (tabId != null) {
// Fetch page settings to ensure the state matches the engine.
context.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = tabId,
operation = TranslationOperation.FETCH_PAGE_SETTINGS,
),
)
}
},
)
}
/**
* Updates the never translate site settings with the [EngineSession] and ensures the global
* list of never translate sites remains in sync.
*
* If an error occurs, then the method will request the page settings be re-fetched and set on
* the browser store.
*
* Note: This method should be used when on the same page as the requested change.
*
* @param context The context used to request the page settings.
* @param tabId Tab ID associated with the request.
* @param setting The value of the site setting to update.
*/
private fun updateNeverTranslateSitePageSetting(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
setting: Boolean,
) {
val engineSession = context.store.state.findTab(tabId)
?.engineState?.engineSession
if (engineSession == null) {
logger.error("Did not receive an engine session to set the never translate site preference.")
} else {
engineSession.setNeverTranslateSiteSetting(
setting = setting,
onResult = {
logger.info("Successfully updated the never translate site preference.")
// Ensure the global sites store is in-sync with the page settings.
context.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = tabId,
operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
),
)
},
onException = {
logger.error("Could not update the never translate site preference.", it)
// Fetch page settings to ensure the state matches the engine.
context.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = tabId,
operation = TranslationOperation.FETCH_PAGE_SETTINGS,
),
)
},
)
}
}
/**
* Helper to find the default "from" language for a site using the page detected language and
* engine supported languages.
*
* @param context The context used to request the information from the store.
* @param tabId Tab ID associated with the request.
* @return The default expected translate "from" language, which is the page language or null
* if unavailable or an unsupported language by the engine.
*/
private fun getDefaultFromLanguage(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
): Language? {
val pageLang = context.store.state.findTab(tabId)
?.translationsState?.translationEngineState?.detectedLanguages?.documentLangTag ?: return null
val supportedLanguages = context.store.state.translationEngine.supportedLanguages ?: return null
return supportedLanguages.findLanguage(pageLang)
}
/**
* Helper to find the default "to" language using the user's preferred language and
* engine supported languages.
*
* @param context The context used to request the information from the store.
* @param tabId Tab ID associated with the request.
* @return The default translate "to" language, which is the user's preferred language or null
* if unavailable or an unsupported language by the engine.
*/
private fun getDefaultToLanguage(
context: MiddlewareContext<BrowserState, BrowserAction>,
tabId: String,
): Language? {
val userPreferredLang = context.store.state.findTab(tabId)
?.translationsState?.translationEngineState?.detectedLanguages?.userPreferredLangTag ?: return null
val supportedLanguages = context.store.state.translationEngine.supportedLanguages ?: return null
return supportedLanguages.findLanguage(userPreferredLang)
}
/**
* Requests the language model updates occur on the [Engine].
*
* Examples of operations include downloading and deleting individual models, all models,
* or the cache.
*
* @param context The context used to update the language models.
* @param options The change and specified language models that should change state.
*/
private fun updateLanguageModel(
context: MiddlewareContext<BrowserState, BrowserAction>,
options: ModelManagementOptions,
) {
logger.info("Requesting the translations engine update the language model(s).")
engine.manageTranslationsLanguageModel(
options = options,
onSuccess = {
// Value was set to a wait state in [TranslationsStateReducer] for
// [TranslationsBrowserState.languageModels], so we need to resolve the state.
val processState = if (options.operation == ModelOperation.DOWNLOAD) {
ModelState.DOWNLOADED
} else {
ModelState.NOT_DOWNLOADED
}
val newModelState = LanguageModel.determineNewLanguageModelState(
appLanguage = context.store.state.locale?.language.toString(),
currentLanguageModels = context.store.state.translationEngine.languageModels,
options = options,
newStatus = processState,
)
if (newModelState != null) {
context.store.dispatch(
TranslationsAction.SetLanguageModelsAction(
languageModels = newModelState,
),
)
if (!areModelsProcessing(newModelState)) {
// Refresh state to ensure we have the latest model sizes.
// Sizes can change if pivots are required and acquired or deleted.
requestLanguageModels(context)
}
logger.info("Successfully updated the language model(s).")
} else {
logger.warn(
"The model(s) were updated with the engine, " +
"but unexpectedly could not update state. " +
"Re-requesting state be retrieved from the engine.",
)
// Unexpectedly lost state, so check with the engine to put it back in-sync.
requestLanguageModels(context)
}
},
onError = { error ->
logger.error("Could not update the language model(s).", error)
// Value was set to a wait state in [TranslationsStateReducer] for
// [TranslationsBrowserState.languageModels], so we need to set an error state.
val errorState = if (options.operation == ModelOperation.DOWNLOAD) {
ModelState.ERROR_DOWNLOAD
} else {
ModelState.ERROR_DELETION
}
val errorModelState = LanguageModel.determineNewLanguageModelState(
appLanguage = context.store.state.locale?.language.toString(),
currentLanguageModels = context.store.state.translationEngine.languageModels,
options = options,
newStatus = errorState,
)
if (errorModelState != null) {
context.store.dispatch(
TranslationsAction.SetLanguageModelsAction(
languageModels = errorModelState,
),
)
logger.info("Successfully set the language model(s) error state.")
} else {
logger.warn(
"Unexpectedly could not update error state. " +
"Re-requesting state be retrieved from the engine.",
)
// Unexpectedly lost state, so check with the engine to put it back in-sync.
requestLanguageModels(context)
}
context.store.dispatch(
TranslationsAction.EngineExceptionAction(
error = TranslationError.LanguageModelUpdateError(error),
),
)
},
)
}
}