Translate: Add DeepL support (#2721)
Co-authored-by: v <vendicated@riseup.net>
This commit is contained in:
parent
2382294e8b
commit
f8b01c1a31
9 changed files with 277 additions and 37 deletions
|
@ -17,10 +17,9 @@
|
|||
*/
|
||||
|
||||
import { ChatBarButton } from "@api/ChatButtons";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { Alerts, Forms } from "@webpack/common";
|
||||
import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { TranslateModal } from "./TranslateModal";
|
||||
|
@ -39,9 +38,17 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
|
|||
);
|
||||
}
|
||||
|
||||
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
|
||||
|
||||
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
||||
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
|
||||
|
||||
const [shouldShowTranslateEnabledTooltip, setter] = useState(false);
|
||||
useEffect(() => {
|
||||
setShouldShowTranslateEnabledTooltip = setter;
|
||||
return () => setShouldShowTranslateEnabledTooltip = undefined;
|
||||
}, []);
|
||||
|
||||
if (!isMainChat || !showChatBarButton) return null;
|
||||
|
||||
const toggle = () => {
|
||||
|
@ -52,21 +59,20 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
|||
title: "Vencord Auto-Translate Enabled",
|
||||
body: <>
|
||||
<Forms.FormText>
|
||||
You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent.
|
||||
</Forms.FormText>
|
||||
<Forms.FormText className={Margins.top16}>
|
||||
If this was an accident, disable it again, or it will change your message content before sending.
|
||||
You just enabled Auto Translate! Any message <b>will automatically be translated</b> before being sent.
|
||||
</Forms.FormText>
|
||||
</>,
|
||||
cancelText: "Disable Auto-Translate",
|
||||
confirmText: "Got it",
|
||||
confirmText: "Disable Auto-Translate",
|
||||
cancelText: "Got it",
|
||||
secondaryConfirmText: "Don't show again",
|
||||
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
|
||||
onCancel: () => settings.store.autoTranslate = false
|
||||
onConfirm: () => settings.store.autoTranslate = false,
|
||||
// troll
|
||||
confirmColor: "vc-notification-log-danger-btn",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
const button = (
|
||||
<ChatBarButton
|
||||
tooltip="Open Translate Modal"
|
||||
onClick={e => {
|
||||
|
@ -76,7 +82,7 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
|||
<TranslateModal rootProps={props} />
|
||||
));
|
||||
}}
|
||||
onContextMenu={() => toggle()}
|
||||
onContextMenu={toggle}
|
||||
buttonProps={{
|
||||
"aria-haspopup": "dialog"
|
||||
}}
|
||||
|
@ -84,4 +90,13 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
|||
<TranslateIcon className={cl({ "auto-translate": autoTranslate, "chat-button": true })} />
|
||||
</ChatBarButton>
|
||||
);
|
||||
|
||||
if (shouldShowTranslateEnabledTooltip && settings.store.showAutoTranslateTooltip)
|
||||
return (
|
||||
<Tooltip text="Auto Translate Enabled" forceOpen>
|
||||
{() => button}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return button;
|
||||
};
|
||||
|
|
|
@ -20,9 +20,8 @@ import { Margins } from "@utils/margins";
|
|||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { Forms, SearchableSelect, Switch, useMemo } from "@webpack/common";
|
||||
|
||||
import { Languages } from "./languages";
|
||||
import { settings } from "./settings";
|
||||
import { cl } from "./utils";
|
||||
import { cl, getLanguages } from "./utils";
|
||||
|
||||
const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const;
|
||||
|
||||
|
@ -31,7 +30,7 @@ function LanguageSelect({ settingsKey, includeAuto }: { settingsKey: typeof Lang
|
|||
|
||||
const options = useMemo(
|
||||
() => {
|
||||
const options = Object.entries(Languages).map(([value, label]) => ({ value, label }));
|
||||
const options = Object.entries(getLanguages()).map(([value, label]) => ({ value, label }));
|
||||
if (!includeAuto)
|
||||
options.shift();
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import { Parser, useEffect, useState } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
import { Languages } from "./languages";
|
||||
import { TranslateIcon } from "./TranslateIcon";
|
||||
import { cl, TranslationValue } from "./utils";
|
||||
|
||||
|
@ -59,7 +58,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
|
|||
<TranslateIcon width={16} height={16} />
|
||||
{Parser.parse(translation.text)}
|
||||
{" "}
|
||||
(translated from {Languages[translation.src] ?? translation.src} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
|
||||
(translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import definePlugin from "@utils/types";
|
|||
import { ChannelStore, Menu } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
|
||||
import { setShouldShowTranslateEnabledTooltip, TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
|
||||
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
|
||||
import { translate } from "./utils";
|
||||
|
||||
|
@ -53,8 +53,8 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) =>
|
|||
|
||||
export default definePlugin({
|
||||
name: "Translate",
|
||||
description: "Translate messages with Google Translate",
|
||||
authors: [Devs.Ven],
|
||||
description: "Translate messages with Google Translate or DeepL",
|
||||
authors: [Devs.Ven, Devs.AshtonMemer],
|
||||
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
|
||||
settings,
|
||||
contextMenus: {
|
||||
|
@ -83,11 +83,18 @@ export default definePlugin({
|
|||
};
|
||||
});
|
||||
|
||||
let tooltipTimeout: any;
|
||||
this.preSend = addPreSendListener(async (_, message) => {
|
||||
if (!settings.store.autoTranslate) return;
|
||||
if (!message.content) return;
|
||||
|
||||
message.content = (await translate("sent", message.content)).text;
|
||||
setShouldShowTranslateEnabledTooltip?.(true);
|
||||
clearTimeout(tooltipTimeout);
|
||||
tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);
|
||||
|
||||
const trans = await translate("sent", message.content);
|
||||
message.content = trans.text;
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -31,9 +31,10 @@ copy(Object.fromEntries(
|
|||
))
|
||||
*/
|
||||
|
||||
export type Language = keyof typeof Languages;
|
||||
export type GoogleLanguage = keyof typeof GoogleLanguages;
|
||||
export type DeeplLanguage = keyof typeof DeeplLanguages;
|
||||
|
||||
export const Languages = {
|
||||
export const GoogleLanguages = {
|
||||
"auto": "Detect language",
|
||||
"af": "Afrikaans",
|
||||
"sq": "Albanian",
|
||||
|
@ -169,3 +170,57 @@ export const Languages = {
|
|||
"yo": "Yoruba",
|
||||
"zu": "Zulu"
|
||||
} as const;
|
||||
|
||||
export const DeeplLanguages = {
|
||||
"": "Detect language",
|
||||
"ar": "Arabic",
|
||||
"bg": "Bulgarian",
|
||||
"zh-hans": "Chinese (Simplified)",
|
||||
"zh-hant": "Chinese (Traditional)",
|
||||
"cs": "Czech",
|
||||
"da": "Danish",
|
||||
"nl": "Dutch",
|
||||
"en-us": "English (American)",
|
||||
"en-gb": "English (British)",
|
||||
"et": "Estonian",
|
||||
"fi": "Finnish",
|
||||
"fr": "French",
|
||||
"de": "German",
|
||||
"el": "Greek",
|
||||
"hu": "Hungarian",
|
||||
"id": "Indonesian",
|
||||
"it": "Italian",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"lv": "Latvian",
|
||||
"lt": "Lithuanian",
|
||||
"nb": "Norwegian",
|
||||
"pl": "Polish",
|
||||
"pt-br": "Portuguese (Brazilian)",
|
||||
"pt-pt": "Portuguese (European)",
|
||||
"ro": "Romanian",
|
||||
"ru": "Russian",
|
||||
"sk": "Slovak",
|
||||
"sl": "Slovenian",
|
||||
"es": "Spanish",
|
||||
"sv": "Swedish",
|
||||
"tr": "Turkish",
|
||||
"uk": "Ukrainian"
|
||||
} as const;
|
||||
|
||||
export function deeplLanguageToGoogleLanguage(language: string) {
|
||||
switch (language) {
|
||||
case "": return "auto";
|
||||
case "nb": return "no";
|
||||
case "zh-hans": return "zh-CN";
|
||||
case "zh-hant": return "zh-TW";
|
||||
case "en-us":
|
||||
case "en-gb":
|
||||
return "en";
|
||||
case "pt-br":
|
||||
case "pt-pt":
|
||||
return "pt";
|
||||
default:
|
||||
return language;
|
||||
}
|
||||
}
|
||||
|
|
29
src/plugins/translate/native.ts
Normal file
29
src/plugins/translate/native.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { IpcMainInvokeEvent } from "electron";
|
||||
|
||||
export async function makeDeeplTranslateRequest(_: IpcMainInvokeEvent, pro: boolean, apiKey: string, payload: string) {
|
||||
const url = pro
|
||||
? "https://api.deepl.com/v2/translate"
|
||||
: "https://api-free.deepl.com/v2/translate";
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `DeepL-Auth-Key ${apiKey}`
|
||||
},
|
||||
body: payload
|
||||
});
|
||||
|
||||
const data = await res.text();
|
||||
return { status: res.status, data };
|
||||
} catch (e) {
|
||||
return { status: -1, data: String(e) };
|
||||
}
|
||||
}
|
|
@ -22,38 +22,76 @@ import { OptionType } from "@utils/types";
|
|||
export const settings = definePluginSettings({
|
||||
receivedInput: {
|
||||
type: OptionType.STRING,
|
||||
description: "Input language for received messages",
|
||||
description: "Language that received messages should be translated from",
|
||||
default: "auto",
|
||||
hidden: true
|
||||
},
|
||||
receivedOutput: {
|
||||
type: OptionType.STRING,
|
||||
description: "Output language for received messages",
|
||||
description: "Language that received messages should be translated to",
|
||||
default: "en",
|
||||
hidden: true
|
||||
},
|
||||
sentInput: {
|
||||
type: OptionType.STRING,
|
||||
description: "Input language for sent messages",
|
||||
description: "Language that your own messages should be translated from",
|
||||
default: "auto",
|
||||
hidden: true
|
||||
},
|
||||
sentOutput: {
|
||||
type: OptionType.STRING,
|
||||
description: "Output language for sent messages",
|
||||
description: "Language that your own messages should be translated to",
|
||||
default: "en",
|
||||
hidden: true
|
||||
},
|
||||
|
||||
showChatBarButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show translate button in chat bar",
|
||||
default: true
|
||||
},
|
||||
service: {
|
||||
type: OptionType.SELECT,
|
||||
description: IS_WEB ? "Translation service (Not supported on Web!)" : "Translation service",
|
||||
disabled: () => IS_WEB,
|
||||
options: [
|
||||
{ label: "Google Translate", value: "google", default: true },
|
||||
{ label: "DeepL Free", value: "deepl" },
|
||||
{ label: "DeepL Pro", value: "deepl-pro" }
|
||||
] as const,
|
||||
onChange: resetLanguageDefaults
|
||||
},
|
||||
deeplApiKey: {
|
||||
type: OptionType.STRING,
|
||||
description: "DeepL API key",
|
||||
default: "",
|
||||
placeholder: "Get your API key from https://deepl.com/your-account",
|
||||
disabled: () => IS_WEB
|
||||
},
|
||||
autoTranslate: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
||||
default: false
|
||||
},
|
||||
showChatBarButton: {
|
||||
showAutoTranslateTooltip: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show translate button in chat bar",
|
||||
description: "Show a tooltip on the ChatBar button whenever a message is automatically translated",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
}).withPrivateSettings<{
|
||||
showAutoTranslateAlert: boolean;
|
||||
}>();
|
||||
|
||||
export function resetLanguageDefaults() {
|
||||
if (IS_WEB || settings.store.service === "google") {
|
||||
settings.store.receivedInput = "auto";
|
||||
settings.store.receivedOutput = "en";
|
||||
settings.store.sentInput = "auto";
|
||||
settings.store.sentOutput = "en";
|
||||
} else {
|
||||
settings.store.receivedInput = "";
|
||||
settings.store.receivedOutput = "en-us";
|
||||
settings.store.sentInput = "";
|
||||
settings.store.sentOutput = "en-us";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,18 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { onlyOnce } from "@utils/onlyOnce";
|
||||
import { PluginNative } from "@utils/types";
|
||||
import { showToast, Toasts } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from "./languages";
|
||||
import { resetLanguageDefaults, settings } from "./settings";
|
||||
|
||||
export const cl = classNameFactory("vc-trans-");
|
||||
|
||||
interface TranslationData {
|
||||
const Native = VencordNative.pluginHelpers.Translate as PluginNative<typeof import("./native")>;
|
||||
|
||||
interface GoogleData {
|
||||
src: string;
|
||||
sentences: {
|
||||
// 🏳️⚧️
|
||||
|
@ -30,15 +36,47 @@ interface TranslationData {
|
|||
}[];
|
||||
}
|
||||
|
||||
interface DeeplData {
|
||||
translations: {
|
||||
detected_source_language: string;
|
||||
text: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TranslationValue {
|
||||
src: string;
|
||||
sourceLanguage: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> {
|
||||
const sourceLang = settings.store[kind + "Input"];
|
||||
const targetLang = settings.store[kind + "Output"];
|
||||
export const getLanguages = () => IS_WEB || settings.store.service === "google"
|
||||
? GoogleLanguages
|
||||
: DeeplLanguages;
|
||||
|
||||
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> {
|
||||
const translate = IS_WEB || settings.store.service === "google"
|
||||
? googleTranslate
|
||||
: deeplTranslate;
|
||||
|
||||
try {
|
||||
return await translate(
|
||||
text,
|
||||
settings.store[`${kind}Input`],
|
||||
settings.store[`${kind}Output`]
|
||||
);
|
||||
} catch (e) {
|
||||
const userMessage = typeof e === "string"
|
||||
? e
|
||||
: "Something went wrong. If this issue persists, please check the console or ask for help in the support server.";
|
||||
|
||||
showToast(userMessage, Toasts.Type.FAILURE);
|
||||
|
||||
throw e instanceof Error
|
||||
? e
|
||||
: new Error(userMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async function googleTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
|
||||
const url = "https://translate.googleapis.com/translate_a/single?" + new URLSearchParams({
|
||||
// see https://stackoverflow.com/a/29537590 for more params
|
||||
// holy shidd nvidia
|
||||
|
@ -63,13 +101,69 @@ export async function translate(kind: "received" | "sent", text: string): Promis
|
|||
+ `\n${res.status} ${res.statusText}`
|
||||
);
|
||||
|
||||
const { src, sentences }: TranslationData = await res.json();
|
||||
const { src, sentences }: GoogleData = await res.json();
|
||||
|
||||
return {
|
||||
src,
|
||||
sourceLanguage: GoogleLanguages[src] ?? src,
|
||||
text: sentences.
|
||||
map(s => s?.trans).
|
||||
filter(Boolean).
|
||||
join("")
|
||||
};
|
||||
}
|
||||
|
||||
function fallbackToGoogle(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
|
||||
return googleTranslate(
|
||||
text,
|
||||
deeplLanguageToGoogleLanguage(sourceLang),
|
||||
deeplLanguageToGoogleLanguage(targetLang)
|
||||
);
|
||||
}
|
||||
|
||||
const showDeeplApiQuotaToast = onlyOnce(
|
||||
() => showToast("Deepl API quota exceeded. Falling back to Google Translate", Toasts.Type.FAILURE)
|
||||
);
|
||||
|
||||
async function deeplTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
|
||||
if (!settings.store.deeplApiKey) {
|
||||
showToast("DeepL API key is not set. Resetting to Google", Toasts.Type.FAILURE);
|
||||
|
||||
settings.store.service = "google";
|
||||
resetLanguageDefaults();
|
||||
|
||||
return fallbackToGoogle(text, sourceLang, targetLang);
|
||||
}
|
||||
|
||||
// CORS jumpscare
|
||||
const { status, data } = await Native.makeDeeplTranslateRequest(
|
||||
settings.store.service === "deepl-pro",
|
||||
settings.store.deeplApiKey,
|
||||
JSON.stringify({
|
||||
text: [text],
|
||||
target_lang: targetLang,
|
||||
source_lang: sourceLang.split("-")[0]
|
||||
})
|
||||
);
|
||||
|
||||
switch (status) {
|
||||
case 200:
|
||||
break;
|
||||
case -1:
|
||||
throw "Failed to connect to DeepL API: " + data;
|
||||
case 403:
|
||||
throw "Invalid DeepL API key or version";
|
||||
case 456:
|
||||
showDeeplApiQuotaToast();
|
||||
return fallbackToGoogle(text, sourceLang, targetLang);
|
||||
default:
|
||||
throw new Error(`Failed to translate "${text}" (${sourceLang} -> ${targetLang})\n${status} ${data}`);
|
||||
}
|
||||
|
||||
const { translations }: DeeplData = JSON.parse(data);
|
||||
const src = translations[0].detected_source_language;
|
||||
|
||||
return {
|
||||
sourceLanguage: DeeplLanguages[src] ?? src,
|
||||
text: translations[0].text
|
||||
};
|
||||
}
|
||||
|
|
|
@ -538,6 +538,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Joona",
|
||||
id: 297410829589020673n
|
||||
},
|
||||
AshtonMemer: {
|
||||
name: "AshtonMemer",
|
||||
id: 373657230530052099n
|
||||
},
|
||||
surgedevs: {
|
||||
name: "Chloe",
|
||||
id: 1084592643784331324n
|
||||
|
|
Loading…
Reference in a new issue