Vencord/src/plugins/translate/utils.ts
Ashton f8b01c1a31
Translate: Add DeepL support (#2721)
Co-authored-by: v <vendicated@riseup.net>
2024-08-01 14:10:27 +02:00

169 lines
5.3 KiB
TypeScript

/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { onlyOnce } from "@utils/onlyOnce";
import { PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from "./languages";
import { resetLanguageDefaults, settings } from "./settings";
export const cl = classNameFactory("vc-trans-");
const Native = VencordNative.pluginHelpers.Translate as PluginNative<typeof import("./native")>;
interface GoogleData {
src: string;
sentences: {
// 🏳️‍⚧️
trans: string;
}[];
}
interface DeeplData {
translations: {
detected_source_language: string;
text: string;
}[];
}
export interface TranslationValue {
sourceLanguage: string;
text: string;
}
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
client: "gtx",
// source language
sl: sourceLang,
// target language
tl: targetLang,
// what to return, t = translation probably
dt: "t",
// Send json object response instead of weird array
dj: "1",
source: "input",
// query, duh
q: text
});
const res = await fetch(url);
if (!res.ok)
throw new Error(
`Failed to translate "${text}" (${sourceLang} -> ${targetLang})`
+ `\n${res.status} ${res.statusText}`
);
const { src, sentences }: GoogleData = await res.json();
return {
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
};
}