import type React from "react" ; import { observable } from "mobx" ; import { isProduction } from "../common-vars" ; import { autoBind , createLogger , JsonResponseError } from "../utils" ; import { MessageId , ProxyRequestPayload , ProxyResponseType } from "../extension/messages" ; import { chromeTtsPlay , chromeTtsStop , getTranslationFromHistory , proxyRequest , saveToHistory } from "../extension/actions" ; import { settingsStore } from "../components/settings/settings.storage" ; export interface TranslatorLanguages { from : Record < string , string > & { auto ? : string ; } ; to ? : Record < string , string > ; } export interface TranslateParams { from : string ; to : string ; text : string ; } export interface TranslatePayload extends TranslateParams { vendor : string ; } export abstract class Translator { static readonly vendors = observable . map < string , Translator > ( ) ; static readonly logger = createLogger ( { systemPrefix : "[TRANSLATOR]" } ) ; abstract name : string ; abstract title : string ; abstract publicUrl : string ; abstract apiUrl : string ; public langFrom : Record < string , string > = { } ; public langTo : Record < string , string > = { } ; public audio : HTMLAudioElement ; constructor ( { from : langFrom , to : langTo } ) { autoBind ( this ) ; const { auto , ... langToFallback } = langFrom ; this . langFrom = langFrom ; this . langTo = langTo ?? langToFallback ; this . translate = new Proxy ( this . translate , { apply : async ( translate , callContext , [ params ] ) : Promise < ITranslationResult > => { return this . #handleTranslation ( translate , params ) ; } } ) ; } abstract translate ( params ) ; async #handleTranslation ( originalTranslate , params ) { let translation = await this . searchResultInHistory ( { vendor : this . name , ... params } ) ; if ( translation ) { Translator . logger . info ( `serving result from history` , params ) ; } if ( ! translation ) { const getResult = async ( customParams = { } ) => { const customizedParams = { ... params , ... customParams } ; const result = await Reflect . apply ( originalTranslate , this , [ customizedParams ] ) ; return this . normalize ( result , customizedParams ) ; } ; translation = await getResult ( ) ; const swapLangParams = this . swapLangCheck ( translation ) ; if ( swapLangParams ) { translation = await getResult ( swapLangParams ) ; } } return this . handleSideEffects ( translation ) ; } protected async searchResultInHistory ( params ) { if ( params . from === "auto" ) { return ; } const cachedResult = await getTranslationFromHistory ( params ) ; if ( cachedResult ) { return this . normalize ( cachedResult , params ) ; } } protected handleSideEffects ( translation ) { const { langDetected , originalText } = translation ; const { autoPlayText , historyEnabled } = settingsStore . data ; if ( autoPlayText ) this . speak ( langDetected , originalText ) ; if ( historyEnabled ) saveToHistory ( translation ) ; return translation ; } protected normalize ( result , initParams ) { const { from : langFrom , to : langTo , text : originalText } = initParams ; const langDetected = result . langDetected ?? ( langFrom !== "auto" ? langFrom : null ) ; return { ... result , vendor : this . name , dictionary : result . dictionary ?? [ ] , langFrom , langDetected , langTo , originalText , } ; } protected swapLangCheck ( normalizedResult ) { var { langTo , langFrom , langDetected , originalText , translation } = normalizedResult ; var sameTextResult = originalText . trim ( ) . toLowerCase ( ) === translation . toLowerCase ( ) . trim ( ) ; if ( sameTextResult && langDetected === langTo ) { return { from : langDetected , to : langFrom !== "auto" ? langFrom : "en" , } ; } } ; static latestRequestId : MessageId ; protected async request ( payload ) { const messageId = Translator . latestRequestId = Math . random ( ) * Date . now ( ) ; const response = await proxyRequest ( payload , messageId ) ; const isLatest = Translator . latestRequestId === response . messageId ; if ( ! isProduction ) { console . log ( '[REQUEST]:' , { payload , response , isLatest } ) ; } if ( ! isLatest ) throw null ; if ( response . data ) return response . data ; throw response . error ; } getLangPairShortTitle ( langFrom , langTo ) { return [ langFrom , langTo ] . join ( ' → ' ) . toUpperCase ( ) ; } getLangPairTitle ( langFrom , langTo ) { return [ this . langFrom [ langFrom ] , this . langTo [ langTo ] ] . join ( ' → ' ) ; } getFullPageTranslationUrl ( pageUrl , lang ) { return null ; } async speak ( lang , text ) { this . stopSpeaking ( ) ; const audioUrl = this . getAudioUrl ( lang , text ) ; const useChromeTtsEngine = Boolean ( settingsStore . data . useChromeTtsEngine || ! audioUrl ) ; Translator . logger . info ( `[TTS]: speaking in lang="${ lang }" text="${ text }"` , { useChromeTtsEngine , } ) ; if ( useChromeTtsEngine ) { if ( lang === "en" ) lang = "en-US" ; chromeTtsPlay ( { lang , text } ) ; } else if ( audioUrl ) { try { this . audio = document . createElement ( "audio" ) ; this . audio . src = await this . request ( { url : audioUrl , responseType : ProxyResponseType . DATA_URI , requestInit : { credentials : "include" , } , } ) ; await this . audio . play ( ) ; } catch ( error ) { Translator . logger . error ( `[TTS]: failed to play: ${ error }` , { lang , text } ) ; } } } static stopSpeaking ( ) { Translator . logger . info ( `[TTS]: stop speaking` ) ; getTranslators ( ) . forEach ( vendor => vendor . audio ?. pause ( ) ) ; chromeTtsStop ( ) ; } stopSpeaking ( ) { this . audio ?. pause ( ) ; Translator . stopSpeaking ( ) ; } getAudioUrl ( lang , text ) { return ; } canTranslate ( langFrom , langTo ) { return ! ! ( this . langFrom [ langFrom ] && this . langTo [ langTo ] ) ; } renderSettingsListWidget ( ) { return null ; } } export interface ITranslationResult { vendor ? : string ; originalText ? : string ; langFrom ? : string ; langTo ? : string ; translation : string ; langDetected ? : string ; transcription ? : string ; spellCorrection ? : string ; dictionary ? : ITranslationDictionary [ ] ; } export interface ITranslationDictionary { wordType : string ; transcription ? : string ; meanings : ITranslationDictionaryMeaning [ ] ; } export interface ITranslationDictionaryMeaning { word : string ; translation : string [ ] ; examples ? : string [ ] [ ] ; } export interface ITranslationError extends JsonResponseError { } export function isTranslationError ( error ) { return error ?. statusCode > 0 && error ?. statusCode < 500 ; } export function isRTL ( lang ) { return [ "ar" , "he" , "iw" , "fa" , "ur" , ] . includes ( lang ) ; } export function getTranslators ( ) { return Array . from ( Translator . vendors . values ( ) ) ; } export function getTranslator ( name ) { return Translator . vendors . get ( name ) ; } export function getNextTranslator ( name , langFrom , langTo , reverse = false ) { var vendors = getTranslators ( ) ; var vendor ; var list = [ ] ; var index = vendors . findIndex ( vendor => vendor . name === name ) ; var beforeCurrent = vendors . slice ( 0 , index ) ; var afterCurrent = vendors . slice ( index + 1 ) ; if ( reverse ) { list . push ( ... beforeCurrent . reverse ( ) , ... afterCurrent . reverse ( ) ) ; } else { list . push ( ... afterCurrent , ... beforeCurrent ) ; } while ( ( vendor = list . shift ( ) ) ) { if ( vendor . canTranslate ( langFrom , langTo ) ) return vendor ; } return null ; }