2023-07-20 19:32:15 +02:00
import {
callPopup ,
eventSource ,
event _types ,
getRequestHeaders ,
reloadCurrentChat ,
saveSettingsDebounced ,
substituteParams ,
updateMessageBlock ,
} from "../../../script.js" ;
import { extension _settings , getContext } from "../../extensions.js" ;
import { secret _state , writeSecret } from "../../secrets.js" ;
const autoModeOptions = {
NONE : 'none' ,
RESPONSES : 'responses' ,
INPUT : 'inputs' ,
BOTH : 'both' ,
} ;
const incomingTypes = [ autoModeOptions . RESPONSES , autoModeOptions . BOTH ] ;
const outgoingTypes = [ autoModeOptions . INPUT , autoModeOptions . BOTH ] ;
const defaultSettings = {
target _language : 'en' ,
internal _language : 'en' ,
provider : 'google' ,
auto _mode : autoModeOptions . NONE ,
} ;
const languageCodes = {
'Afrikaans' : 'af' ,
'Albanian' : 'sq' ,
'Amharic' : 'am' ,
'Arabic' : 'ar' ,
'Armenian' : 'hy' ,
'Azerbaijani' : 'az' ,
'Basque' : 'eu' ,
'Belarusian' : 'be' ,
'Bengali' : 'bn' ,
'Bosnian' : 'bs' ,
'Bulgarian' : 'bg' ,
'Catalan' : 'ca' ,
'Cebuano' : 'ceb' ,
'Chinese (Simplified)' : 'zh-CN' ,
'Chinese (Traditional)' : 'zh-TW' ,
'Corsican' : 'co' ,
'Croatian' : 'hr' ,
'Czech' : 'cs' ,
'Danish' : 'da' ,
'Dutch' : 'nl' ,
'English' : 'en' ,
'Esperanto' : 'eo' ,
'Estonian' : 'et' ,
'Finnish' : 'fi' ,
'French' : 'fr' ,
'Frisian' : 'fy' ,
'Galician' : 'gl' ,
'Georgian' : 'ka' ,
'German' : 'de' ,
'Greek' : 'el' ,
'Gujarati' : 'gu' ,
'Haitian Creole' : 'ht' ,
'Hausa' : 'ha' ,
'Hawaiian' : 'haw' ,
'Hebrew' : 'iw' ,
'Hindi' : 'hi' ,
'Hmong' : 'hmn' ,
'Hungarian' : 'hu' ,
'Icelandic' : 'is' ,
'Igbo' : 'ig' ,
'Indonesian' : 'id' ,
'Irish' : 'ga' ,
'Italian' : 'it' ,
'Japanese' : 'ja' ,
'Javanese' : 'jw' ,
'Kannada' : 'kn' ,
'Kazakh' : 'kk' ,
'Khmer' : 'km' ,
'Korean' : 'ko' ,
'Kurdish' : 'ku' ,
'Kyrgyz' : 'ky' ,
'Lao' : 'lo' ,
'Latin' : 'la' ,
'Latvian' : 'lv' ,
'Lithuanian' : 'lt' ,
'Luxembourgish' : 'lb' ,
'Macedonian' : 'mk' ,
'Malagasy' : 'mg' ,
'Malay' : 'ms' ,
'Malayalam' : 'ml' ,
'Maltese' : 'mt' ,
'Maori' : 'mi' ,
'Marathi' : 'mr' ,
'Mongolian' : 'mn' ,
'Myanmar (Burmese)' : 'my' ,
'Nepali' : 'ne' ,
'Norwegian' : 'no' ,
'Nyanja (Chichewa)' : 'ny' ,
'Pashto' : 'ps' ,
'Persian' : 'fa' ,
'Polish' : 'pl' ,
'Portuguese (Portugal, Brazil)' : 'pt' ,
'Punjabi' : 'pa' ,
'Romanian' : 'ro' ,
'Russian' : 'ru' ,
'Samoan' : 'sm' ,
'Scots Gaelic' : 'gd' ,
'Serbian' : 'sr' ,
'Sesotho' : 'st' ,
'Shona' : 'sn' ,
'Sindhi' : 'sd' ,
'Sinhala (Sinhalese)' : 'si' ,
'Slovak' : 'sk' ,
'Slovenian' : 'sl' ,
'Somali' : 'so' ,
'Spanish' : 'es' ,
'Sundanese' : 'su' ,
'Swahili' : 'sw' ,
'Swedish' : 'sv' ,
'Tagalog (Filipino)' : 'tl' ,
'Tajik' : 'tg' ,
'Tamil' : 'ta' ,
'Telugu' : 'te' ,
'Thai' : 'th' ,
'Turkish' : 'tr' ,
'Ukrainian' : 'uk' ,
'Urdu' : 'ur' ,
'Uzbek' : 'uz' ,
'Vietnamese' : 'vi' ,
'Welsh' : 'cy' ,
'Xhosa' : 'xh' ,
'Yiddish' : 'yi' ,
'Yoruba' : 'yo' ,
'Zulu' : 'zu' ,
} ;
2023-08-26 18:53:57 +02:00
const KEY _REQUIRED = [ 'deepl' , 'libre' ] ;
const LOCAL _URL = [ 'libre' ] ;
2023-07-20 19:32:15 +02:00
2023-08-26 18:53:57 +02:00
function showKeysButton ( ) {
2023-07-20 19:32:15 +02:00
const providerRequiresKey = KEY _REQUIRED . includes ( extension _settings . translate . provider ) ;
2023-08-26 18:53:57 +02:00
const providerOptionalUrl = LOCAL _URL . includes ( extension _settings . translate . provider ) ;
2023-07-20 19:32:15 +02:00
$ ( "#translate_key_button" ) . toggle ( providerRequiresKey ) ;
$ ( "#translate_key_button" ) . toggleClass ( 'success' , Boolean ( secret _state [ extension _settings . translate . provider ] ) ) ;
2023-08-26 18:53:57 +02:00
$ ( "#translate_url_button" ) . toggle ( providerOptionalUrl ) ;
2023-07-20 19:32:15 +02:00
}
function loadSettings ( ) {
for ( const key in defaultSettings ) {
if ( ! extension _settings . translate . hasOwnProperty ( key ) ) {
extension _settings . translate [ key ] = defaultSettings [ key ] ;
}
}
$ ( ` #translation_provider option[value=" ${ extension _settings . translate . provider } "] ` ) . attr ( 'selected' , true ) ;
$ ( ` #translation_target_language option[value=" ${ extension _settings . translate . target _language } "] ` ) . attr ( 'selected' , true ) ;
$ ( ` #translation_auto_mode option[value=" ${ extension _settings . translate . auto _mode } "] ` ) . attr ( 'selected' , true ) ;
2023-08-26 18:53:57 +02:00
showKeysButton ( ) ;
2023-07-20 19:32:15 +02:00
}
async function translateImpersonate ( text ) {
const translatedText = await translate ( text , extension _settings . translate . target _language ) ;
$ ( "#send_textarea" ) . val ( translatedText ) ;
}
async function translateIncomingMessage ( messageId ) {
const context = getContext ( ) ;
const message = context . chat [ messageId ] ;
if ( typeof message . extra !== 'object' ) {
message . extra = { } ;
}
// New swipe is being generated. Don't translate that
if ( $ ( ` #chat .mes[mesid=" ${ messageId } "] .mes_text ` ) . text ( ) == '...' ) {
return ;
}
const textToTranslate = substituteParams ( message . mes , context . name1 , message . name ) ;
const translation = await translate ( textToTranslate , extension _settings . translate . target _language ) ;
message . extra . display _text = translation ;
updateMessageBlock ( messageId , message ) ;
}
2023-08-26 18:53:57 +02:00
async function translateProviderLibre ( text , lang ) {
const response = await fetch ( '/libre_translate' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { text : text , lang : lang } ) ,
} ) ;
if ( response . ok ) {
const result = await response . text ( ) ;
return result ;
}
throw new Error ( response . statusText ) ;
}
2023-07-20 19:32:15 +02:00
async function translateProviderGoogle ( text , lang ) {
const response = await fetch ( '/google_translate' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { text : text , lang : lang } ) ,
} ) ;
if ( response . ok ) {
const result = await response . text ( ) ;
return result ;
}
throw new Error ( response . statusText ) ;
}
async function translateProviderDeepl ( text , lang ) {
if ( ! secret _state . deepl ) {
throw new Error ( 'No DeepL API key' ) ;
}
const response = await fetch ( '/deepl_translate' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { text : text , lang : lang } ) ,
} ) ;
if ( response . ok ) {
const result = await response . text ( ) ;
return result ;
}
throw new Error ( response . statusText ) ;
}
async function translate ( text , lang ) {
try {
2023-08-23 09:32:48 +02:00
if ( text == '' ) {
return '' ;
}
2023-07-20 19:32:15 +02:00
switch ( extension _settings . translate . provider ) {
2023-08-26 18:53:57 +02:00
case 'libre' :
return await translateProviderLibre ( text , lang ) ;
2023-07-20 19:32:15 +02:00
case 'google' :
return await translateProviderGoogle ( text , lang ) ;
case 'deepl' :
return await translateProviderDeepl ( text , lang ) ;
default :
console . error ( 'Unknown translation provider' , extension _settings . translate . provider ) ;
return text ;
}
} catch ( error ) {
console . log ( error ) ;
toastr . error ( String ( error ) , 'Failed to translate message' ) ;
}
}
async function translateOutgoingMessage ( messageId ) {
const context = getContext ( ) ;
const message = context . chat [ messageId ] ;
if ( typeof message . extra !== 'object' ) {
message . extra = { } ;
}
const originalText = message . mes ;
message . extra . display _text = originalText ;
message . mes = await translate ( originalText , extension _settings . translate . internal _language ) ;
updateMessageBlock ( messageId , message ) ;
console . log ( 'translateOutgoingMessage' , messageId ) ;
}
function shouldTranslate ( types ) {
return types . includes ( extension _settings . translate . auto _mode ) ;
}
function createEventHandler ( translateFunction , shouldTranslateFunction ) {
return async ( data ) => {
if ( shouldTranslateFunction ( ) ) {
await translateFunction ( data ) ;
}
} ;
}
// Prevents the chat from being translated in parallel
let translateChatExecuting = false ;
async function onTranslateChatClick ( ) {
if ( translateChatExecuting ) {
return ;
}
try {
translateChatExecuting = true ;
const context = getContext ( ) ;
const chat = context . chat ;
toastr . info ( ` ${ chat . length } message(s) queued for translation. ` , 'Please wait...' ) ;
for ( let i = 0 ; i < chat . length ; i ++ ) {
await translateIncomingMessage ( i ) ;
}
await context . saveChat ( ) ;
} catch ( error ) {
console . log ( error ) ;
toastr . error ( 'Failed to translate chat' ) ;
} finally {
translateChatExecuting = false ;
}
}
async function onTranslationsClearClick ( ) {
const confirm = await callPopup ( '<h3>Are you sure?</h3>This will remove translated text from all messages in the current chat. This action cannot be undone.' , 'confirm' ) ;
if ( ! confirm ) {
return ;
}
const context = getContext ( ) ;
const chat = context . chat ;
for ( const mes of chat ) {
if ( mes . extra ) {
delete mes . extra . display _text ;
}
}
await context . saveChat ( ) ;
await reloadCurrentChat ( ) ;
}
async function translateMessageEdit ( messageId ) {
const context = getContext ( ) ;
const chat = context . chat ;
const message = chat [ messageId ] ;
if ( message . is _system || extension _settings . translate . auto _mode == autoModeOptions . NONE ) {
return ;
}
if ( ( message . is _user && shouldTranslate ( outgoingTypes ) ) || ( ! message . is _user && shouldTranslate ( incomingTypes ) ) ) {
await translateIncomingMessage ( messageId ) ;
}
}
async function onMessageTranslateClick ( ) {
const context = getContext ( ) ;
const messageId = $ ( this ) . closest ( '.mes' ) . attr ( 'mesid' ) ;
const message = context . chat [ messageId ] ;
// If the message is already translated, revert it back to the original text
if ( message ? . extra ? . display _text ) {
delete message . extra . display _text ;
updateMessageBlock ( messageId , message ) ;
}
// If the message is not translated, translate it
else {
await translateIncomingMessage ( messageId ) ;
}
await context . saveChat ( ) ;
}
const handleIncomingMessage = createEventHandler ( translateIncomingMessage , ( ) => shouldTranslate ( incomingTypes ) ) ;
const handleOutgoingMessage = createEventHandler ( translateOutgoingMessage , ( ) => shouldTranslate ( outgoingTypes ) ) ;
const handleImpersonateReady = createEventHandler ( translateImpersonate , ( ) => shouldTranslate ( incomingTypes ) ) ;
const handleMessageEdit = createEventHandler ( translateMessageEdit , ( ) => true ) ;
jQuery ( ( ) => {
const html = `
< div class = "translation_settings" >
< div class = "inline-drawer" >
< div class = "inline-drawer-toggle inline-drawer-header" >
< b > Chat Translation < / b >
< div class = "inline-drawer-icon fa-solid fa-circle-chevron-down down" > < / d i v >
< / d i v >
< div class = "inline-drawer-content" >
< label for = "translation_auto_mode" class = "checkbox_label" > Auto - mode < / l a b e l >
< select id = "translation_auto_mode" >
< option value = "none" > None < / o p t i o n >
< option value = "responses" > Translate responses < / o p t i o n >
< option value = "inputs" > Translate inputs < / o p t i o n >
< option value = "both" > Translate both < / o p t i o n >
< / s e l e c t >
< label for = "translation_provider" > Provider < / l a b e l >
< div class = "flex-container gap5px flexnowrap marginBot5" >
< select id = "translation_provider" name = "provider" class = "margin0" >
2023-08-26 18:53:57 +02:00
< option value = "libre" > Libre < / o p t i o n >
2023-07-20 19:32:15 +02:00
< option value = "google" > Google < / o p t i o n >
< option value = "deepl" > DeepL < / o p t i o n >
< select >
< div id = "translate_key_button" class = "menu_button fa-solid fa-key margin0" > < / d i v >
2023-08-26 18:53:57 +02:00
< div id = "translate_url_button" class = "menu_button fa-solid fa-link margin0" > < / d i v >
2023-07-20 19:32:15 +02:00
< / d i v >
< label for = "translation_target_language" > Target Language < / l a b e l >
< select id = "translation_target_language" name = "target_language" > < / s e l e c t >
< div id = "translation_clear" class = "menu_button" >
< i class = "fa-solid fa-trash-can" > < / i >
< span > Clear Translations < / s p a n >
< / d i v >
< / d i v >
< / d i v >
< / d i v > ` ;
const buttonHtml = `
< div id = "translate_chat" class = "list-group-item flex-container flexGap5" >
< div class = "fa-solid fa-language extensionsMenuExtensionButton" / > < / d i v >
Translate Chat
< / d i v > ` ;
$ ( '#extensionsMenu' ) . append ( buttonHtml ) ;
$ ( '#extensions_settings2' ) . append ( html ) ;
$ ( '#translate_chat' ) . on ( 'click' , onTranslateChatClick ) ;
$ ( '#translation_clear' ) . on ( 'click' , onTranslationsClearClick ) ;
for ( const [ key , value ] of Object . entries ( languageCodes ) ) {
$ ( '#translation_target_language' ) . append ( ` <option value=" ${ value } "> ${ key } </option> ` ) ;
}
$ ( '#translation_auto_mode' ) . on ( 'change' , ( event ) => {
extension _settings . translate . auto _mode = event . target . value ;
saveSettingsDebounced ( ) ;
} ) ;
$ ( '#translation_provider' ) . on ( 'change' , ( event ) => {
extension _settings . translate . provider = event . target . value ;
2023-08-26 18:53:57 +02:00
showKeysButton ( ) ;
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( ) ;
} ) ;
$ ( '#translation_target_language' ) . on ( 'change' , ( event ) => {
extension _settings . translate . target _language = event . target . value ;
saveSettingsDebounced ( ) ;
} ) ;
$ ( document ) . on ( 'click' , '.mes_translate' , onMessageTranslateClick ) ;
$ ( '#translate_key_button' ) . on ( 'click' , async ( ) => {
const optionText = $ ( '#translation_provider option:selected' ) . text ( ) ;
const key = await callPopup ( ` <h3> ${ optionText } API Key</h3> ` , 'input' ) ;
if ( key == false ) {
return ;
}
await writeSecret ( extension _settings . translate . provider , key ) ;
toastr . success ( 'API Key saved' ) ;
} ) ;
2023-08-26 18:53:57 +02:00
$ ( '#translate_url_button' ) . on ( 'click' , async ( ) => {
const optionText = $ ( '#translation_provider option:selected' ) . text ( ) ;
2023-09-05 22:30:58 +02:00
const exampleURLs = { } ;
exampleURLs [ 'libre' ] = 'http://127.0.0.1:5000/translate' ;
const url = await callPopup ( ` <h3> ${ optionText } API URL</h3><i>Example: <tt> ` + exampleURLs [ extension _settings . translate . provider ] + ` </tt></i> ` , 'input' ) ;
2023-08-26 18:53:57 +02:00
if ( url == false ) {
return ;
}
await writeSecret ( extension _settings . translate . provider + "_url" , url ) ;
toastr . success ( 'API URL saved' ) ;
} ) ;
2023-07-20 19:32:15 +02:00
loadSettings ( ) ;
2023-08-22 21:45:12 +02:00
eventSource . on ( event _types . CHARACTER _MESSAGE _RENDERED , handleIncomingMessage ) ;
2023-07-20 19:32:15 +02:00
eventSource . on ( event _types . MESSAGE _SWIPED , handleIncomingMessage ) ;
2023-08-22 21:45:12 +02:00
eventSource . on ( event _types . USER _MESSAGE _RENDERED , handleOutgoingMessage ) ;
2023-07-20 19:32:15 +02:00
eventSource . on ( event _types . IMPERSONATE _READY , handleImpersonateReady ) ;
eventSource . on ( event _types . MESSAGE _EDITED , handleMessageEdit ) ;
document . body . classList . add ( 'translate' ) ;
} ) ;