migrate
This commit is contained in:
8
public/scripts/extensions/translate/buttons.html
Normal file
8
public/scripts/extensions/translate/buttons.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div id="translate_chat" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-language extensionsMenuExtensionButton"></div>
|
||||
<span data-i18n="ext_translate_btn_chat">Translate Chat</span>
|
||||
</div>
|
||||
<div id="translate_input_message" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-keyboard extensionsMenuExtensionButton"></div>
|
||||
<span data-i18n="ext_translate_btn_input">Translate Input</span>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
<h3 data-i18n="ext_translate_delete_confirm_1">Are you sure?</h3><span data-i18n="ext_translate_delete_confirm_2">This will remove translated text from all messages in the current chat. This action cannot be undone.</span>
|
42
public/scripts/extensions/translate/index.html
Normal file
42
public/scripts/extensions/translate/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<div class="translation_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="ext_translate_title">Chat Translation</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="translation_auto_mode" class="checkbox_label" data-i18n="ext_translate_auto_mode">Auto-mode</label>
|
||||
<select id="translation_auto_mode">
|
||||
<option data-i18n="ext_translate_mode_none" value="none">None</option>
|
||||
<option data-i18n="ext_translate_mode_responses" value="responses">Translate responses</option>
|
||||
<option data-i18n="ext_translate_mode_inputs" value="inputs">Translate inputs</option>
|
||||
<option data-i18n="ext_translate_mode_both" value="both">Translate both</option>
|
||||
</select>
|
||||
<label data-i18n="ext_translate_mode_provider" for="translation_provider">Provider</label>
|
||||
<div class="flex-container gap5px flexnowrap marginBot5">
|
||||
<select id="translation_provider" name="provider" class="margin0 text_pole flex2">
|
||||
<option value="libre">LibreTranslate</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="lingva">Lingva</option>
|
||||
<option value="deepl">DeepL API</option>
|
||||
<option value="deeplx">DeepLX</option>
|
||||
<option value="bing">Bing</option>
|
||||
<option value="oneringtranslator">OneRingTranslator</option>
|
||||
<option value="yandex">Yandex</option>
|
||||
<select>
|
||||
<select id="deepl_api_endpoint" class="margin0 text_pole flex1" title="DeepL API Endpoint">
|
||||
<option value="free">Free</option>
|
||||
<option value="pro">Pro</option>
|
||||
</select>
|
||||
<div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>
|
||||
<div id="translate_url_button" class="menu_button fa-solid fa-link margin0"></div>
|
||||
</div>
|
||||
<label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>
|
||||
<select id="translation_target_language" name="target_language"></select>
|
||||
<div id="translation_clear" class="menu_button">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
<span data-i18n="ext_translate_clear">Clear Translations</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
849
public/scripts/extensions/translate/index.js
Normal file
849
public/scripts/extensions/translate/index.js
Normal file
@@ -0,0 +1,849 @@
|
||||
export { translate };
|
||||
|
||||
import {
|
||||
eventSource,
|
||||
event_types,
|
||||
getRequestHeaders,
|
||||
reloadCurrentChat,
|
||||
saveSettingsDebounced,
|
||||
substituteParams,
|
||||
updateMessageBlock,
|
||||
} from '../../../script.js';
|
||||
import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { updateReasoningUI } from '../../reasoning.js';
|
||||
import { findSecret, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
|
||||
export 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,
|
||||
deepl_endpoint: 'free',
|
||||
};
|
||||
|
||||
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)': 'pt-PT',
|
||||
'Portuguese (Brazil)': 'pt-BR',
|
||||
'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',
|
||||
};
|
||||
|
||||
const KEY_REQUIRED = ['deepl', 'libre'];
|
||||
const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva'];
|
||||
|
||||
function showKeysButton() {
|
||||
const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
|
||||
const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);
|
||||
$('#translate_key_button').toggle(providerRequiresKey);
|
||||
$('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
|
||||
$('#translate_url_button').toggle(providerOptionalUrl);
|
||||
$('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
|
||||
$('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
for (const key in defaultSettings) {
|
||||
if (!Object.hasOwn(extension_settings.translate, 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');
|
||||
$('#deepl_api_endpoint').val(extension_settings.translate.deepl_endpoint).toggle(extension_settings.translate.provider === 'deepl');
|
||||
showKeysButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the swipe is being generated for a message.
|
||||
* @param {string|number} messageId Message ID
|
||||
* @returns {boolean} Whether the swipe is being generated
|
||||
*/
|
||||
function isGeneratingSwipe(messageId) {
|
||||
return $(`#chat .mes[mesid="${messageId}"] .mes_text`).text() === '...';
|
||||
}
|
||||
|
||||
async function translateImpersonate(text) {
|
||||
const translatedText = await translate(text, extension_settings.translate.target_language);
|
||||
$('#send_textarea').val(translatedText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the contents of an incoming message.
|
||||
* @param {string | number} messageId Message ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function translateIncomingMessage(messageId) {
|
||||
const context = getContext();
|
||||
const message = context.chat[messageId];
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof message.extra !== 'object') {
|
||||
message.extra = {};
|
||||
}
|
||||
|
||||
if (isGeneratingSwipe(messageId)) {
|
||||
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(Number(messageId), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the reasoning of an incoming message.
|
||||
* @param {string | number} messageId
|
||||
* @returns {Promise<boolean>} translated or not
|
||||
*/
|
||||
async function translateIncomingMessageReasoning(messageId) {
|
||||
const context = getContext();
|
||||
const message = context.chat[messageId];
|
||||
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof message.extra !== 'object') {
|
||||
message.extra = {};
|
||||
}
|
||||
|
||||
if (!message.extra.reasoning || isGeneratingSwipe(messageId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const textToTranslate = substituteParams(message.extra.reasoning, context.name1, message.name);
|
||||
const translation = await translate(textToTranslate, extension_settings.translate.target_language);
|
||||
message.extra.reasoning_display_text = translation;
|
||||
|
||||
updateReasoningUI(Number(messageId));
|
||||
return true;
|
||||
}
|
||||
|
||||
async function translateProviderOneRing(text, lang) {
|
||||
let from_lang = lang == extension_settings.translate.internal_language
|
||||
? extension_settings.translate.target_language
|
||||
: extension_settings.translate.internal_language;
|
||||
|
||||
const response = await fetch('/api/translate/onering', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ text: text, from_lang: from_lang, to_lang: lang }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.text();
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the LibreTranslate API
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderLibre(text, lang) {
|
||||
const response = await fetch('/api/translate/libre', {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the Google Translate API
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderGoogle(text, lang) {
|
||||
const response = await fetch('/api/translate/google', {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using an instance of the Lingva Translate
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderLingva(text, lang) {
|
||||
const response = await fetch('/api/translate/lingva', {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the DeepL API
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderDeepl(text, lang) {
|
||||
if (!secret_state.deepl) {
|
||||
throw new Error('No DeepL API key');
|
||||
}
|
||||
|
||||
const endpoint = extension_settings.translate.deepl_endpoint || 'free';
|
||||
const response = await fetch('/api/translate/deepl', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ text: text, lang: lang, endpoint: endpoint }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.text();
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the DeepLX API
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderDeepLX(text, lang) {
|
||||
const response = await fetch('/api/translate/deeplx', {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the Bing API
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderBing(text, lang) {
|
||||
const response = await fetch('/api/translate/bing', {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the Yandex Translate API
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateProviderYandex(text, lang) {
|
||||
let chunks = [];
|
||||
const chunkSize = 5000;
|
||||
if (text.length <= chunkSize) {
|
||||
chunks.push(text);
|
||||
} else {
|
||||
chunks = splitRecursive(text, chunkSize);
|
||||
}
|
||||
const response = await fetch('/api/translate/yandex', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ chunks: chunks, lang: lang }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.text();
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits text into chunks and translates each chunk separately
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {(text: string, lang: string) => Promise<string>} translateFn Function to translate a single chunk (must return a Promise)
|
||||
* @param {number} chunkSize Maximum chunk size
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function chunkedTranslate(text, lang, translateFn, chunkSize = 5000) {
|
||||
if (text.length <= chunkSize) {
|
||||
return await translateFn(text, lang);
|
||||
}
|
||||
|
||||
const chunks = splitRecursive(text, chunkSize);
|
||||
|
||||
let result = '';
|
||||
for (const chunk of chunks) {
|
||||
result += await translateFn(chunk, lang);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the selected translation provider
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {string} provider Translation provider to use
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translate(text, lang, provider = null) {
|
||||
try {
|
||||
if (text == '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!lang) {
|
||||
lang = extension_settings.translate.target_language;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
provider = extension_settings.translate.provider;
|
||||
}
|
||||
|
||||
// split text by embedded images links
|
||||
const chunks = text.split(/!\[.*?]\([^)]*\)/);
|
||||
const links = [...text.matchAll(/!\[.*?]\([^)]*\)/g)];
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
result += await translateInner(chunks[i], lang, provider);
|
||||
if (i < links.length) result += links[i][0];
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toastr.error(String(error), 'Failed to translate message');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common translation function that handles the translation logic
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {string} provider Translation provider to use
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateInner(text, lang, provider) {
|
||||
if (text == '') {
|
||||
return '';
|
||||
}
|
||||
if (!provider) {
|
||||
provider = extension_settings.translate.provider;
|
||||
}
|
||||
switch (provider) {
|
||||
case 'libre':
|
||||
return await translateProviderLibre(text, lang);
|
||||
case 'google':
|
||||
return await chunkedTranslate(text, lang, translateProviderGoogle, 5000);
|
||||
case 'lingva':
|
||||
return await chunkedTranslate(text, lang, translateProviderLingva, 5000);
|
||||
case 'deepl':
|
||||
return await translateProviderDeepl(text, lang);
|
||||
case 'deeplx':
|
||||
return await chunkedTranslate(text, lang, translateProviderDeepLX, 1500);
|
||||
case 'oneringtranslator':
|
||||
return await translateProviderOneRing(text, lang);
|
||||
case 'bing':
|
||||
return await chunkedTranslate(text, lang, translateProviderBing, 1000);
|
||||
case 'yandex':
|
||||
return await translateProviderYandex(text, lang);
|
||||
default:
|
||||
console.error('Unknown translation provider', provider);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function onTranslateInputMessageClick() {
|
||||
const textarea = document.getElementById('send_textarea');
|
||||
|
||||
if (!(textarea instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!textarea.value) {
|
||||
toastr.warning('Enter a message first');
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = toastr.info('Input Message is translating', 'Please wait...');
|
||||
const translatedText = await translate(textarea.value, extension_settings.translate.internal_language);
|
||||
textarea.value = translatedText;
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
toastr.clear(toast);
|
||||
}
|
||||
|
||||
// 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 translateIncomingMessageReasoning(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 popupHtml = await renderExtensionTemplateAsync('translate', 'deleteConfirmation');
|
||||
const confirm = await callGenericPopup(popupHtml, POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
|
||||
for (const mes of chat) {
|
||||
if (mes.extra) {
|
||||
delete mes.extra.display_text;
|
||||
delete mes.extra.reasoning_display_text;
|
||||
}
|
||||
}
|
||||
|
||||
await context.saveChat();
|
||||
await reloadCurrentChat();
|
||||
}
|
||||
|
||||
async function translateMessageEdit(messageId) {
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
const message = chat[messageId];
|
||||
|
||||
let anyChange = false;
|
||||
if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.display_text)) {
|
||||
delete message.extra.display_text;
|
||||
updateMessageBlock(messageId, message);
|
||||
anyChange = true;
|
||||
} else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
|
||||
await translateIncomingMessage(messageId);
|
||||
anyChange = true;
|
||||
}
|
||||
|
||||
if (anyChange) {
|
||||
await context.saveChat();
|
||||
}
|
||||
}
|
||||
|
||||
async function translateMessageReasoningEdit(messageId) {
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
const message = chat[messageId];
|
||||
|
||||
let anyChange = false;
|
||||
if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.reasoning_display_text)) {
|
||||
delete message.extra.reasoning_display_text;
|
||||
updateReasoningUI(Number(messageId));
|
||||
anyChange = true;
|
||||
} else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
|
||||
anyChange = await translateIncomingMessageReasoning(messageId);
|
||||
}
|
||||
|
||||
if (anyChange) {
|
||||
await context.saveChat();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeReasoningDisplayText(messageId) {
|
||||
const context = getContext();
|
||||
const message = context.chat[messageId];
|
||||
if (message.extra?.reasoning_display_text) {
|
||||
delete message.extra.reasoning_display_text;
|
||||
updateReasoningUI(Number(messageId));
|
||||
await context.saveChat();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
let alreadyTranslated = false;
|
||||
if (message?.extra?.display_text) {
|
||||
delete message.extra.display_text;
|
||||
updateMessageBlock(Number(messageId), message);
|
||||
alreadyTranslated = true;
|
||||
}
|
||||
if (message?.extra?.reasoning_display_text) {
|
||||
delete message.extra.reasoning_display_text;
|
||||
updateReasoningUI(Number(messageId));
|
||||
alreadyTranslated = true;
|
||||
}
|
||||
|
||||
// If the message is not translated, translate it
|
||||
if (!alreadyTranslated) {
|
||||
await translateIncomingMessageReasoning(messageId);
|
||||
await translateIncomingMessage(messageId);
|
||||
}
|
||||
|
||||
await context.saveChat();
|
||||
}
|
||||
|
||||
const handleIncomingMessage = createEventHandler(async (messageId) => {
|
||||
await translateIncomingMessageReasoning(messageId);
|
||||
await translateIncomingMessage(messageId);
|
||||
}, () => shouldTranslate(incomingTypes));
|
||||
const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes));
|
||||
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
|
||||
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
|
||||
const handleMessageReasoningEdit = createEventHandler(translateMessageReasoningEdit, () => true);
|
||||
const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayText, () => true);
|
||||
|
||||
globalThis.translate = translate;
|
||||
|
||||
jQuery(async () => {
|
||||
const html = await renderExtensionTemplateAsync('translate', 'index');
|
||||
const buttonHtml = await renderExtensionTemplateAsync('translate', 'buttons');
|
||||
|
||||
$('#translate_wand_container').append(buttonHtml);
|
||||
$('#translation_container').append(html);
|
||||
$('#translate_chat').on('click', onTranslateChatClick);
|
||||
$('#translate_input_message').on('click', onTranslateInputMessageClick);
|
||||
$('#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) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.auto_mode = event.target.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#translation_provider').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.provider = event.target.value;
|
||||
showKeysButton();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#translation_target_language').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.target_language = event.target.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#deepl_api_endpoint').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.deepl_endpoint = 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 callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT, '', {
|
||||
customButtons: [{
|
||||
text: 'Remove Key',
|
||||
appendAtEnd: true,
|
||||
result: POPUP_RESULT.NEGATIVE,
|
||||
action: async () => {
|
||||
await writeSecret(extension_settings.translate.provider, '');
|
||||
toastr.success('API Key removed');
|
||||
$('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeSecret(extension_settings.translate.provider, key);
|
||||
toastr.success('API Key saved');
|
||||
$('#translate_key_button').addClass('success');
|
||||
});
|
||||
$('#translate_url_button').on('click', async () => {
|
||||
const optionText = $('#translation_provider option:selected').text();
|
||||
const exampleURLs = {
|
||||
'libre': 'http://127.0.0.1:5000/translate',
|
||||
'lingva': 'https://lingva.ml/api/v1',
|
||||
'oneringtranslator': 'http://127.0.0.1:4990/translate',
|
||||
'deeplx': 'http://127.0.0.1:1188/translate',
|
||||
};
|
||||
const popupText = `<h3>${optionText} API URL</h3><i>Example: <tt>${String(exampleURLs[extension_settings.translate.provider])}</tt></i>`;
|
||||
|
||||
const secretKey = extension_settings.translate.provider + '_url';
|
||||
const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
|
||||
|
||||
const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl, {
|
||||
customButtons: [{
|
||||
text: 'Remove URL',
|
||||
appendAtEnd: true,
|
||||
result: POPUP_RESULT.NEGATIVE,
|
||||
action: async () => {
|
||||
await writeSecret(secretKey, '');
|
||||
toastr.success('API URL removed');
|
||||
$('#translate_url_button').toggleClass('success', !!secret_state[secretKey]);
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeSecret(secretKey, url);
|
||||
|
||||
toastr.success('API URL saved');
|
||||
$('#translate_url_button').addClass('success');
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
|
||||
eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
|
||||
eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
|
||||
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
|
||||
eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
|
||||
eventSource.on(event_types.MESSAGE_REASONING_EDITED, handleMessageReasoningEdit);
|
||||
eventSource.on(event_types.MESSAGE_REASONING_DELETED, handleMessageReasoningDelete);
|
||||
|
||||
document.body.classList.add('translate');
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'translate',
|
||||
helpString: 'Translate text to a target language. If target language is not provided, the value from the extension settings will be used.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('target', 'The target language code to translate to', ARGUMENT_TYPE.STRING, false, false, '', Object.values(languageCodes)),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'provider',
|
||||
description: 'The translation provider to use. If not provided, the value from the extension settings will be used.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
enumProvider: () => Array.from(document.getElementById('translation_provider').querySelectorAll('option')).map((option) => new SlashCommandEnumValue(option.value, option.text, enumTypes.name, enumIcons.server)),
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The text to translate', ARGUMENT_TYPE.STRING, true, false, ''),
|
||||
],
|
||||
callback: async (args, value) => {
|
||||
const target = args?.target && Object.values(languageCodes).includes(String(args.target))
|
||||
? String(args.target)
|
||||
: extension_settings.translate.target_language;
|
||||
const provider = args?.provider || extension_settings.translate.provider;
|
||||
return await translate(String(value), target, provider);
|
||||
},
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
});
|
11
public/scripts/extensions/translate/manifest.json
Normal file
11
public/scripts/extensions/translate/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Chat Translation",
|
||||
"loading_order": 1,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/ChuQuadrant/ChuQuadrant"
|
||||
}
|
6
public/scripts/extensions/translate/style.css
Normal file
6
public/scripts/extensions/translate/style.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.translation_settings .menu_button {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: row;
|
||||
}
|
Reference in New Issue
Block a user