Blame Raw
Cohee · 51ad27fb · · 804 lines (26.0 KB)
2 contributors
1export { translate };
2
3import {
4 eventSource,
5 event_types,
6 getRequestHeaders,
7 reloadCurrentChat,
8 saveSettingsDebounced,
9 substituteParams,
10 updateMessageBlock,
11} from '../../../script.js';
12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
13import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
14import { updateReasoningUI } from '../../reasoning.js';
15import { secret_state } from '../../secrets.js';
16import { SlashCommand } from '../../slash-commands/SlashCommand.js';
17import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
18import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
19import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
20import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
21import { splitRecursive } from '../../utils.js';
22
23export const autoModeOptions = {
24 NONE: 'none',
25 RESPONSES: 'responses',
26 INPUT: 'inputs',
27 BOTH: 'both',
28};
29
30const incomingTypes = [autoModeOptions.RESPONSES, autoModeOptions.BOTH];
31const outgoingTypes = [autoModeOptions.INPUT, autoModeOptions.BOTH];
32
33const defaultSettings = {
34 target_language: 'en',
35 internal_language: 'en',
36 provider: 'google',
37 auto_mode: autoModeOptions.NONE,
38 deepl_endpoint: 'free',
39};
40
41const languageCodes = {
42 'Afrikaans': 'af',
43 'Albanian': 'sq',
44 'Amharic': 'am',
45 'Arabic': 'ar',
46 'Armenian': 'hy',
47 'Azerbaijani': 'az',
48 'Basque': 'eu',
49 'Belarusian': 'be',
50 'Bengali': 'bn',
51 'Bosnian': 'bs',
52 'Bulgarian': 'bg',
53 'Catalan': 'ca',
54 'Cebuano': 'ceb',
55 'Chinese (Simplified)': 'zh-CN',
56 'Chinese (Traditional)': 'zh-TW',
57 'Corsican': 'co',
58 'Croatian': 'hr',
59 'Czech': 'cs',
60 'Danish': 'da',
61 'Dutch': 'nl',
62 'English': 'en',
63 'Esperanto': 'eo',
64 'Estonian': 'et',
65 'Finnish': 'fi',
66 'French': 'fr',
67 'Frisian': 'fy',
68 'Galician': 'gl',
69 'Georgian': 'ka',
70 'German': 'de',
71 'Greek': 'el',
72 'Gujarati': 'gu',
73 'Haitian Creole': 'ht',
74 'Hausa': 'ha',
75 'Hawaiian': 'haw',
76 'Hebrew': 'iw',
77 'Hindi': 'hi',
78 'Hmong': 'hmn',
79 'Hungarian': 'hu',
80 'Icelandic': 'is',
81 'Igbo': 'ig',
82 'Indonesian': 'id',
83 'Irish': 'ga',
84 'Italian': 'it',
85 'Japanese': 'ja',
86 'Javanese': 'jw',
87 'Kannada': 'kn',
88 'Kazakh': 'kk',
89 'Khmer': 'km',
90 'Korean': 'ko',
91 'Kurdish': 'ku',
92 'Kyrgyz': 'ky',
93 'Lao': 'lo',
94 'Latin': 'la',
95 'Latvian': 'lv',
96 'Lithuanian': 'lt',
97 'Luxembourgish': 'lb',
98 'Macedonian': 'mk',
99 'Malagasy': 'mg',
100 'Malay': 'ms',
101 'Malayalam': 'ml',
102 'Maltese': 'mt',
103 'Maori': 'mi',
104 'Marathi': 'mr',
105 'Mongolian': 'mn',
106 'Myanmar (Burmese)': 'my',
107 'Nepali': 'ne',
108 'Norwegian': 'no',
109 'Nyanja (Chichewa)': 'ny',
110 'Pashto': 'ps',
111 'Persian': 'fa',
112 'Polish': 'pl',
113 'Portuguese (Portugal)': 'pt-PT',
114 'Portuguese (Brazil)': 'pt-BR',
115 'Punjabi': 'pa',
116 'Romanian': 'ro',
117 'Russian': 'ru',
118 'Samoan': 'sm',
119 'Scots Gaelic': 'gd',
120 'Serbian': 'sr',
121 'Sesotho': 'st',
122 'Shona': 'sn',
123 'Sindhi': 'sd',
124 'Sinhala (Sinhalese)': 'si',
125 'Slovak': 'sk',
126 'Slovenian': 'sl',
127 'Somali': 'so',
128 'Spanish': 'es',
129 'Sundanese': 'su',
130 'Swahili': 'sw',
131 'Swedish': 'sv',
132 'Tagalog (Filipino)': 'tl',
133 'Tajik': 'tg',
134 'Tamil': 'ta',
135 'Telugu': 'te',
136 'Thai': 'th',
137 'Turkish': 'tr',
138 'Ukrainian': 'uk',
139 'Urdu': 'ur',
140 'Uzbek': 'uz',
141 'Vietnamese': 'vi',
142 'Welsh': 'cy',
143 'Xhosa': 'xh',
144 'Yiddish': 'yi',
145 'Yoruba': 'yo',
146 'Zulu': 'zu',
147};
148
149const KEY_REQUIRED = ['deepl', 'libre'];
150const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva'];
151
152function showKeysButton() {
153 const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
154 const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);
155 $('#translate_key_button').toggle(providerRequiresKey).data('key', extension_settings.translate.provider);
156 $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
157 $('#translate_url_button').toggle(providerOptionalUrl).data('key', extension_settings.translate.provider + '_url');
158 $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
159 $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');
160}
161
162function loadSettings() {
163 for (const key in defaultSettings) {
164 if (!Object.hasOwn(extension_settings.translate, key)) {
165 extension_settings.translate[key] = defaultSettings[key];
166 }
167 }
168
169 $(`#translation_provider option[value="${extension_settings.translate.provider}"]`).attr('selected', 'true');
170 $(`#translation_target_language option[value="${extension_settings.translate.target_language}"]`).attr('selected', 'true');
171 $(`#translation_auto_mode option[value="${extension_settings.translate.auto_mode}"]`).attr('selected', 'true');
172 $('#deepl_api_endpoint').val(extension_settings.translate.deepl_endpoint).toggle(extension_settings.translate.provider === 'deepl');
173 showKeysButton();
174}
175
176/**
177 * Check if the swipe is being generated for a message.
178 * @param {string|number} messageId Message ID
179 * @returns {boolean} Whether the swipe is being generated
180 */
181function isGeneratingSwipe(messageId) {
182 return $(`#chat .mes[mesid="${messageId}"] .mes_text`).text() === '...';
183}
184
185async function translateImpersonate() {
186 const sendTextArea = $('#send_textarea');
187 const text = sendTextArea.val().toString();
188 const translatedText = await translate(text, extension_settings.translate.target_language);
189 sendTextArea.val(translatedText);
190}
191
192/**
193 * Translates the contents of an incoming message.
194 * @param {string | number} messageId Message ID
195 * @returns {Promise<void>}
196 */
197async function translateIncomingMessage(messageId) {
198 const context = getContext();
199 const message = context.chat[messageId];
200
201 if (!message) {
202 return;
203 }
204
205 if (typeof message.extra !== 'object') {
206 message.extra = {};
207 }
208
209 if (isGeneratingSwipe(messageId)) {
210 return;
211 }
212
213 const textToTranslate = substituteParams(message.mes, { name2Override: message.name });
214 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
215 message.extra.display_text = translation;
216
217 updateMessageBlock(Number(messageId), message);
218}
219
220/**
221 * Translates the reasoning of an incoming message.
222 * @param {string | number} messageId
223 * @returns {Promise<boolean>} translated or not
224 */
225async function translateIncomingMessageReasoning(messageId) {
226 const context = getContext();
227 const message = context.chat[messageId];
228
229 if (!message) {
230 return false;
231 }
232
233 if (typeof message.extra !== 'object') {
234 message.extra = {};
235 }
236
237 if (!message.extra.reasoning || isGeneratingSwipe(messageId)) {
238 return false;
239 }
240
241 const textToTranslate = substituteParams(message.extra.reasoning, { name2Override: message.name });
242 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
243 message.extra.reasoning_display_text = translation;
244
245 updateReasoningUI(Number(messageId));
246 return true;
247}
248
249async function translateProviderOneRing(text, lang) {
250 let from_lang = lang == extension_settings.translate.internal_language
251 ? extension_settings.translate.target_language
252 : extension_settings.translate.internal_language;
253
254 const response = await fetch('/api/translate/onering', {
255 method: 'POST',
256 headers: getRequestHeaders(),
257 body: JSON.stringify({ text: text, from_lang: from_lang, to_lang: lang }),
258 });
259
260 if (response.ok) {
261 const result = await response.text();
262 return result;
263 }
264
265 throw new Error(response.statusText);
266}
267
268/**
269 * Translates text using the LibreTranslate API
270 * @param {string} text Text to translate
271 * @param {string} lang Target language code
272 * @returns {Promise<string>} Translated text
273 */
274async function translateProviderLibre(text, lang) {
275 const response = await fetch('/api/translate/libre', {
276 method: 'POST',
277 headers: getRequestHeaders(),
278 body: JSON.stringify({ text: text, lang: lang }),
279 });
280
281 if (response.ok) {
282 const result = await response.text();
283 return result;
284 }
285
286 throw new Error(response.statusText);
287}
288
289/**
290 * Translates text using the Google Translate API
291 * @param {string} text Text to translate
292 * @param {string} lang Target language code
293 * @returns {Promise<string>} Translated text
294 */
295async function translateProviderGoogle(text, lang) {
296 const response = await fetch('/api/translate/google', {
297 method: 'POST',
298 headers: getRequestHeaders(),
299 body: JSON.stringify({ text: text, lang: lang }),
300 });
301
302 if (response.ok) {
303 const result = await response.text();
304 return result;
305 }
306
307 throw new Error(response.statusText);
308}
309
310/**
311 * Translates text using an instance of the Lingva Translate
312 * @param {string} text Text to translate
313 * @param {string} lang Target language code
314 * @returns {Promise<string>} Translated text
315 */
316async function translateProviderLingva(text, lang) {
317 const response = await fetch('/api/translate/lingva', {
318 method: 'POST',
319 headers: getRequestHeaders(),
320 body: JSON.stringify({ text: text, lang: lang }),
321 });
322
323 if (response.ok) {
324 const result = await response.text();
325 return result;
326 }
327
328 throw new Error(response.statusText);
329}
330
331/**
332 * Translates text using the DeepL API
333 * @param {string} text Text to translate
334 * @param {string} lang Target language code
335 * @returns {Promise<string>} Translated text
336 */
337async function translateProviderDeepl(text, lang) {
338 if (!secret_state.deepl) {
339 throw new Error('No DeepL API key');
340 }
341
342 const endpoint = extension_settings.translate.deepl_endpoint || 'free';
343 const response = await fetch('/api/translate/deepl', {
344 method: 'POST',
345 headers: getRequestHeaders(),
346 body: JSON.stringify({ text: text, lang: lang, endpoint: endpoint }),
347 });
348
349 if (response.ok) {
350 const result = await response.text();
351 return result;
352 }
353
354 throw new Error(response.statusText);
355}
356
357/**
358 * Translates text using the DeepLX API
359 * @param {string} text Text to translate
360 * @param {string} lang Target language code
361 * @returns {Promise<string>} Translated text
362 */
363async function translateProviderDeepLX(text, lang) {
364 const response = await fetch('/api/translate/deeplx', {
365 method: 'POST',
366 headers: getRequestHeaders(),
367 body: JSON.stringify({ text: text, lang: lang }),
368 });
369
370 if (response.ok) {
371 const result = await response.text();
372 return result;
373 }
374
375 throw new Error(response.statusText);
376}
377
378/**
379 * Translates text using the Bing API
380 * @param {string} text Text to translate
381 * @param {string} lang Target language code
382 * @returns {Promise<string>} Translated text
383 */
384async function translateProviderBing(text, lang) {
385 const response = await fetch('/api/translate/bing', {
386 method: 'POST',
387 headers: getRequestHeaders(),
388 body: JSON.stringify({ text: text, lang: lang }),
389 });
390
391 if (response.ok) {
392 const result = await response.text();
393 return result;
394 }
395
396 throw new Error(response.statusText);
397}
398
399/**
400 * Translates text using the Yandex Translate API
401 * @param {string} text Text to translate
402 * @param {string} lang Target language code
403 * @returns {Promise<string>} Translated text
404 */
405async function translateProviderYandex(text, lang) {
406 let chunks = [];
407 const chunkSize = 5000;
408 if (text.length <= chunkSize) {
409 chunks.push(text);
410 } else {
411 chunks = splitRecursive(text, chunkSize);
412 }
413 const response = await fetch('/api/translate/yandex', {
414 method: 'POST',
415 headers: getRequestHeaders(),
416 body: JSON.stringify({ chunks: chunks, lang: lang }),
417 });
418
419 if (response.ok) {
420 const result = await response.text();
421 return result;
422 }
423
424 throw new Error(response.statusText);
425}
426
427/**
428 * Splits text into chunks and translates each chunk separately
429 * @param {string} text Text to translate
430 * @param {string} lang Target language code
431 * @param {(text: string, lang: string) => Promise<string>} translateFn Function to translate a single chunk (must return a Promise)
432 * @param {number} chunkSize Maximum chunk size
433 * @returns {Promise<string>} Translated text
434 */
435async function chunkedTranslate(text, lang, translateFn, chunkSize = 5000) {
436 if (text.length <= chunkSize) {
437 return await translateFn(text, lang);
438 }
439
440 const chunks = splitRecursive(text, chunkSize);
441
442 let result = '';
443 for (const chunk of chunks) {
444 result += await translateFn(chunk, lang);
445 }
446 return result;
447}
448
449/**
450 * Translates text using the selected translation provider
451 * @param {string} text Text to translate
452 * @param {string} lang Target language code
453 * @param {string} provider Translation provider to use
454 * @returns {Promise<string>} Translated text
455 */
456async function translate(text, lang, provider = null) {
457 try {
458 if (text == '') {
459 return '';
460 }
461
462 if (!lang) {
463 lang = extension_settings.translate.target_language;
464 }
465
466 if (!provider) {
467 provider = extension_settings.translate.provider;
468 }
469
470 // split text by embedded images links
471 const chunks = text.split(/!\[.*?]\([^)]*\)/);
472 const links = [...text.matchAll(/!\[.*?]\([^)]*\)/g)];
473
474 let result = '';
475 for (let i = 0; i < chunks.length; i++) {
476 result += await translateInner(chunks[i], lang, provider);
477 if (i < links.length) result += links[i][0];
478 }
479
480 return result;
481 } catch (error) {
482 console.log(error);
483 toastr.error(String(error), 'Failed to translate message');
484 }
485}
486
487/**
488 * Common translation function that handles the translation logic
489 * @param {string} text Text to translate
490 * @param {string} lang Target language code
491 * @param {string} provider Translation provider to use
492 * @returns {Promise<string>} Translated text
493 */
494async function translateInner(text, lang, provider) {
495 if (text == '') {
496 return '';
497 }
498 if (!provider) {
499 provider = extension_settings.translate.provider;
500 }
501 switch (provider) {
502 case 'libre':
503 return await translateProviderLibre(text, lang);
504 case 'google':
505 return await chunkedTranslate(text, lang, translateProviderGoogle, 5000);
506 case 'lingva':
507 return await chunkedTranslate(text, lang, translateProviderLingva, 5000);
508 case 'deepl':
509 return await translateProviderDeepl(text, lang);
510 case 'deeplx':
511 return await chunkedTranslate(text, lang, translateProviderDeepLX, 1500);
512 case 'oneringtranslator':
513 return await translateProviderOneRing(text, lang);
514 case 'bing':
515 return await chunkedTranslate(text, lang, translateProviderBing, 1000);
516 case 'yandex':
517 return await translateProviderYandex(text, lang);
518 default:
519 console.error('Unknown translation provider', provider);
520 return text;
521 }
522}
523
524async function translateOutgoingMessage(messageId) {
525 const context = getContext();
526 const message = context.chat[messageId];
527
528 if (typeof message.extra !== 'object') {
529 message.extra = {};
530 }
531
532 const originalText = message.mes;
533 message.extra.display_text = originalText;
534 message.mes = await translate(originalText, extension_settings.translate.internal_language);
535 updateMessageBlock(messageId, message);
536
537 console.log('translateOutgoingMessage', messageId);
538}
539
540function shouldTranslate(types) {
541 return types.includes(extension_settings.translate.auto_mode);
542}
543
544function createEventHandler(translateFunction, shouldTranslateFunction) {
545 return async (data) => {
546 if (shouldTranslateFunction()) {
547 await translateFunction(data);
548 }
549 };
550}
551
552async function onTranslateInputMessageClick() {
553 const textarea = document.getElementById('send_textarea');
554
555 if (!(textarea instanceof HTMLTextAreaElement)) {
556 return;
557 }
558
559 if (!textarea.value) {
560 toastr.warning('Enter a message first');
561 return;
562 }
563
564 const toast = toastr.info('Input Message is translating', 'Please wait...');
565 const translatedText = await translate(textarea.value, extension_settings.translate.internal_language);
566 textarea.value = translatedText;
567 textarea.dispatchEvent(new Event('input', { bubbles: true }));
568 toastr.clear(toast);
569}
570
571// Prevents the chat from being translated in parallel
572let translateChatExecuting = false;
573
574async function onTranslateChatClick() {
575 if (translateChatExecuting) {
576 return;
577 }
578
579 try {
580 translateChatExecuting = true;
581 const context = getContext();
582 const chat = context.chat;
583
584 toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...');
585
586 for (let i = 0; i < chat.length; i++) {
587 await translateIncomingMessageReasoning(i);
588 await translateIncomingMessage(i);
589 }
590
591 await context.saveChat();
592 } catch (error) {
593 console.log(error);
594 toastr.error('Failed to translate chat');
595 } finally {
596 translateChatExecuting = false;
597 }
598}
599
600async function onTranslationsClearClick() {
601 const popupHtml = await renderExtensionTemplateAsync('translate', 'deleteConfirmation');
602 const confirm = await callGenericPopup(popupHtml, POPUP_TYPE.CONFIRM);
603
604 if (!confirm) {
605 return;
606 }
607
608 const context = getContext();
609 const chat = context.chat;
610
611 for (const mes of chat) {
612 if (mes.extra) {
613 delete mes.extra.display_text;
614 delete mes.extra.reasoning_display_text;
615 }
616 }
617
618 await context.saveChat();
619 await reloadCurrentChat();
620}
621
622async function translateMessageEdit(messageId) {
623 const context = getContext();
624 const chat = context.chat;
625 const message = chat[messageId];
626
627 let anyChange = false;
628 if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.display_text)) {
629 delete message.extra.display_text;
630 updateMessageBlock(messageId, message);
631 anyChange = true;
632 } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
633 await translateIncomingMessage(messageId);
634 anyChange = true;
635 }
636
637 if (anyChange) {
638 await context.saveChat();
639 }
640}
641
642async function translateMessageReasoningEdit(messageId) {
643 const context = getContext();
644 const chat = context.chat;
645 const message = chat[messageId];
646
647 let anyChange = false;
648 if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.reasoning_display_text)) {
649 delete message.extra.reasoning_display_text;
650 updateReasoningUI(Number(messageId));
651 anyChange = true;
652 } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
653 anyChange = await translateIncomingMessageReasoning(messageId);
654 }
655
656 if (anyChange) {
657 await context.saveChat();
658 }
659}
660
661async function removeReasoningDisplayText(messageId) {
662 const context = getContext();
663 const message = context.chat[messageId];
664 if (message.extra?.reasoning_display_text) {
665 delete message.extra.reasoning_display_text;
666 updateReasoningUI(Number(messageId));
667 await context.saveChat();
668 }
669}
670
671async function onMessageTranslateClick() {
672 const context = getContext();
673 const messageId = $(this).closest('.mes').attr('mesid');
674 const message = context.chat[messageId];
675
676 // If the message is already translated, revert it back to the original text
677 let alreadyTranslated = false;
678 if (message?.extra?.display_text) {
679 delete message.extra.display_text;
680 updateMessageBlock(Number(messageId), message);
681 alreadyTranslated = true;
682 }
683 if (message?.extra?.reasoning_display_text) {
684 delete message.extra.reasoning_display_text;
685 updateReasoningUI(Number(messageId));
686 alreadyTranslated = true;
687 }
688
689 // If the message is not translated, translate it
690 if (!alreadyTranslated) {
691 await translateIncomingMessageReasoning(messageId);
692 await translateIncomingMessage(messageId);
693 }
694
695 await context.saveChat();
696}
697
698const handleIncomingMessage = createEventHandler(async (messageId) => {
699 await translateIncomingMessageReasoning(messageId);
700 await translateIncomingMessage(messageId);
701}, () => shouldTranslate(incomingTypes));
702const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes));
703const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
704const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
705const handleMessageReasoningEdit = createEventHandler(translateMessageReasoningEdit, () => true);
706const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayText, () => true);
707
708globalThis.translate = translate;
709
710export async function init() {
711 const html = await renderExtensionTemplateAsync('translate', 'index');
712 const buttonHtml = await renderExtensionTemplateAsync('translate', 'buttons');
713
714 $('#translate_wand_container').append(buttonHtml);
715 $('#translation_container').append(html);
716 $('#translate_chat').on('click', onTranslateChatClick);
717 $('#translate_input_message').on('click', onTranslateInputMessageClick);
718 $('#translation_clear').on('click', onTranslationsClearClick);
719
720 for (const [key, value] of Object.entries(languageCodes)) {
721 $('#translation_target_language').append(`<option value="${value}">${key}</option>`);
722 }
723
724 $('#translation_auto_mode').on('change', (event) => {
725 if (!(event.target instanceof HTMLSelectElement)) {
726 return;
727 }
728 extension_settings.translate.auto_mode = event.target.value;
729 saveSettingsDebounced();
730 });
731 $('#translation_provider').on('change', (event) => {
732 if (!(event.target instanceof HTMLSelectElement)) {
733 return;
734 }
735 extension_settings.translate.provider = event.target.value;
736 showKeysButton();
737 saveSettingsDebounced();
738 });
739 $('#translation_target_language').on('change', (event) => {
740 if (!(event.target instanceof HTMLSelectElement)) {
741 return;
742 }
743 extension_settings.translate.target_language = event.target.value;
744 saveSettingsDebounced();
745 });
746 $('#deepl_api_endpoint').on('change', (event) => {
747 if (!(event.target instanceof HTMLSelectElement)) {
748 return;
749 }
750 extension_settings.translate.deepl_endpoint = event.target.value;
751 saveSettingsDebounced();
752 });
753 $(document).on('click', '.mes_translate', onMessageTranslateClick);
754
755 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach((eventType) => {
756 eventSource.on(eventType, (/** @type {string} */ key) => {
757 if (key === extension_settings.translate.provider) {
758 $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
759 }
760 if (key === `${extension_settings.translate.provider}_url`) {
761 $('#translate_url_button').toggleClass('success', !!secret_state[`${extension_settings.translate.provider}_url`]);
762 }
763 });
764 });
765
766 loadSettings();
767
768 eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
769 eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
770 eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
771 eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
772 eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
773 eventSource.on(event_types.MESSAGE_REASONING_EDITED, handleMessageReasoningEdit);
774 eventSource.on(event_types.MESSAGE_REASONING_DELETED, handleMessageReasoningDelete);
775
776 document.body.classList.add('translate');
777
778 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
779 name: 'translate',
780 helpString: 'Translate text to a target language. If target language is not provided, the value from the extension settings will be used.',
781 namedArgumentList: [
782 new SlashCommandNamedArgument('target', 'The target language code to translate to', ARGUMENT_TYPE.STRING, false, false, '', Object.values(languageCodes)),
783 SlashCommandNamedArgument.fromProps({
784 name: 'provider',
785 description: 'The translation provider to use. If not provided, the value from the extension settings will be used.',
786 typeList: [ARGUMENT_TYPE.STRING],
787 isRequired: false,
788 acceptsMultiple: false,
789 enumProvider: () => Array.from(document.getElementById('translation_provider').querySelectorAll('option')).map((option) => new SlashCommandEnumValue(option.value, option.text, enumTypes.name, enumIcons.server)),
790 }),
791 ],
792 unnamedArgumentList: [
793 new SlashCommandArgument('The text to translate', ARGUMENT_TYPE.STRING, true, false, ''),
794 ],
795 callback: async (args, value) => {
796 const target = args?.target && Object.values(languageCodes).includes(String(args.target))
797 ? String(args.target)
798 : extension_settings.translate.target_language;
799 const provider = args?.provider || extension_settings.translate.provider;
800 return await translate(String(value), target, provider);
801 },
802 returns: ARGUMENT_TYPE.STRING,
803 }));
804}