aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/translate
diff options
context:
space:
mode:
authorV <vendicated@riseup.net>2023-05-10 23:14:04 +0200
committerGitHub <noreply@github.com>2023-05-10 23:14:04 +0200
commitcb385d1b280551eb16f1f9836a93cc9bcc43da15 (patch)
tree87897b84fe78d4d10686981c8bd1c31a5326b46d /src/plugins/translate
parent195f1a032fc63d4fd35564a9d11f8ed4afbcac4d (diff)
downloadVencord-cb385d1b280551eb16f1f9836a93cc9bcc43da15.tar.gz
Vencord-cb385d1b280551eb16f1f9836a93cc9bcc43da15.tar.bz2
Vencord-cb385d1b280551eb16f1f9836a93cc9bcc43da15.zip
New Plugin: Translate (#1089)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Diffstat (limited to 'src/plugins/translate')
-rw-r--r--src/plugins/translate/TranslateIcon.tsx70
-rw-r--r--src/plugins/translate/TranslateModal.tsx101
-rw-r--r--src/plugins/translate/TranslationAccessory.tsx62
-rw-r--r--src/plugins/translate/index.tsx86
-rw-r--r--src/plugins/translate/languages.ts172
-rw-r--r--src/plugins/translate/settings.ts52
-rw-r--r--src/plugins/translate/styles.css37
-rw-r--r--src/plugins/translate/utils.ts75
8 files changed, 655 insertions, 0 deletions
diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx
new file mode 100644
index 0000000..d944ec1
--- /dev/null
+++ b/src/plugins/translate/TranslateIcon.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { classes } from "@utils/misc";
+import { openModal } from "@utils/modal";
+import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
+
+import { settings } from "./settings";
+import { TranslateModal } from "./TranslateModal";
+import { cl } from "./utils";
+
+export function TranslateIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) {
+ return (
+ <svg
+ viewBox="0 96 960 960"
+ height={height}
+ width={width}
+ className={classes(cl("icon"), className)}
+ >
+ <path fill="currentColor" d="m475 976 181-480h82l186 480h-87l-41-126H604l-47 126h-82Zm151-196h142l-70-194h-2l-70 194Zm-466 76-55-55 204-204q-38-44-67.5-88.5T190 416h87q17 33 37.5 62.5T361 539q45-47 75-97.5T487 336H40v-80h280v-80h80v80h280v80H567q-22 69-58.5 135.5T419 598l98 99-30 81-127-122-200 200Z" />
+ </svg>
+ );
+}
+
+export function TranslateChatBarIcon() {
+ const { autoTranslate } = settings.use(["autoTranslate"]);
+
+ return (
+ <Tooltip text="Open Translate Modal">
+ {({ onMouseEnter, onMouseLeave }) => (
+ <div style={{ display: "flex" }}>
+ <Button
+ aria-haspopup="dialog"
+ aria-label=""
+ size=""
+ look={ButtonLooks.BLANK}
+ onMouseEnter={onMouseEnter}
+ onMouseLeave={onMouseLeave}
+ innerClassName={ButtonWrapperClasses.button}
+ onClick={() =>
+ openModal(props => (
+ <TranslateModal rootProps={props} />
+ ))
+ }
+ style={{ padding: "0 4px" }}
+ >
+ <div className={ButtonWrapperClasses.buttonWrapper}>
+ <TranslateIcon className={cl({ "auto-translate": autoTranslate })} />
+ </div>
+ </Button>
+ </div>
+ )}
+ </Tooltip>
+ );
+}
diff --git a/src/plugins/translate/TranslateModal.tsx b/src/plugins/translate/TranslateModal.tsx
new file mode 100644
index 0000000..7628a31
--- /dev/null
+++ b/src/plugins/translate/TranslateModal.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 { 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";
+
+const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const;
+
+function LanguageSelect({ settingsKey, includeAuto }: { settingsKey: typeof LanguageSettingKeys[number]; includeAuto: boolean; }) {
+ const currentValue = settings.use([settingsKey])[settingsKey];
+
+ const options = useMemo(
+ () => {
+ const options = Object.entries(Languages).map(([value, label]) => ({ value, label }));
+ if (!includeAuto)
+ options.shift();
+
+ return options;
+ }, []
+ );
+
+ return (
+ <section className={Margins.bottom16}>
+ <Forms.FormTitle tag="h3">
+ {settings.def[settingsKey].description}
+ </Forms.FormTitle>
+
+ <SearchableSelect
+ options={options}
+ value={options.find(o => o.value === currentValue)}
+ placeholder={"Select a language"}
+ maxVisibleItems={5}
+ closeOnSelect={true}
+ onChange={v => settings.store[settingsKey] = v}
+ />
+ </section>
+ );
+}
+
+function AutoTranslateToggle() {
+ const value = settings.use(["autoTranslate"]).autoTranslate;
+
+ return (
+ <Switch
+ value={value}
+ onChange={v => settings.store.autoTranslate = v}
+ note={settings.def.autoTranslate.description}
+ hideBorder
+ >
+ Auto Translate
+ </Switch>
+ );
+}
+
+
+export function TranslateModal({ rootProps }: { rootProps: ModalProps; }) {
+ return (
+ <ModalRoot {...rootProps}>
+ <ModalHeader className={cl("modal-header")}>
+ <Forms.FormTitle tag="h2">
+ Translate
+ </Forms.FormTitle>
+ <ModalCloseButton onClick={rootProps.onClose} />
+ </ModalHeader>
+
+ <ModalContent className={cl("modal-content")}>
+ {LanguageSettingKeys.map(s => (
+ <LanguageSelect
+ key={s}
+ settingsKey={s}
+ includeAuto={s.endsWith("Input")}
+ />
+ ))}
+
+ <Forms.FormDivider className={Margins.bottom16} />
+
+ <AutoTranslateToggle />
+ </ModalContent>
+ </ModalRoot>
+ );
+}
diff --git a/src/plugins/translate/TranslationAccessory.tsx b/src/plugins/translate/TranslationAccessory.tsx
new file mode 100644
index 0000000..4f46e4b
--- /dev/null
+++ b/src/plugins/translate/TranslationAccessory.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { 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";
+
+const TranslationSetters = new Map<string, (v: TranslationValue) => void>();
+
+export function handleTranslate(messageId: string, data: TranslationValue) {
+ TranslationSetters.get(messageId)!(data);
+}
+
+function Dismiss({ onDismiss }: { onDismiss: () => void; }) {
+ return (
+ <button
+ onClick={onDismiss}
+ className={cl("dismiss")}
+ >
+ Dismiss
+ </button>
+ );
+}
+
+export function TranslationAccessory({ message }: { message: Message; }) {
+ const [translation, setTranslation] = useState<TranslationValue>();
+
+ useEffect(() => {
+ TranslationSetters.set(message.id, setTranslation);
+
+ return () => void TranslationSetters.delete(message.id);
+ }, []);
+
+ if (!translation) return null;
+
+ return (
+ <span className={cl("accessory")}>
+ <TranslateIcon width={16} height={16} />
+ {Parser.parse(translation.text)}
+ {" "}
+ (translated from {Languages[translation.src] ?? translation.src} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
+ </span>
+ );
+}
diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx
new file mode 100644
index 0000000..cb61254
--- /dev/null
+++ b/src/plugins/translate/index.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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 "./styles.css";
+
+import { addAccessory, removeAccessory } from "@api/MessageAccessories";
+import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
+import { addButton, removeButton } from "@api/MessagePopover";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { ChannelStore } from "@webpack/common";
+
+import { settings } from "./settings";
+import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
+import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
+import { translate } from "./utils";
+
+export default definePlugin({
+ name: "Translate",
+ description: "Translate messages with Google Translate",
+ authors: [Devs.Ven],
+ dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI"],
+ settings,
+ // not used, just here in case some other plugin wants it or w/e
+ translate,
+
+ patches: [
+ {
+ find: ".activeCommandOption",
+ replacement: {
+ match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
+ replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
+ }
+ },
+ ],
+
+ start() {
+ addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
+
+ addButton("vc-translate", message => {
+ if (!message.content) return null;
+
+ return {
+ label: "Translate",
+ icon: TranslateIcon,
+ message,
+ channel: ChannelStore.getChannel(message.channel_id),
+ onClick: async () => {
+ const trans = await translate("received", message.content);
+ handleTranslate(message.id, trans);
+ }
+ };
+ });
+
+ this.preSend = addPreSendListener(async (_, message) => {
+ if (!settings.store.autoTranslate) return;
+ if (!message.content) return;
+
+ message.content = (await translate("sent", message.content)).text;
+ });
+ },
+
+ stop() {
+ removePreSendListener(this.preSend);
+ removeButton("vc-translate");
+ removeAccessory("vc-translation");
+ },
+
+ chatBarIcon: ErrorBoundary.wrap(TranslateChatBarIcon, { noop: true }),
+});
diff --git a/src/plugins/translate/languages.ts b/src/plugins/translate/languages.ts
new file mode 100644
index 0000000..c3be053
--- /dev/null
+++ b/src/plugins/translate/languages.ts
@@ -0,0 +1,172 @@
+/*
+ * 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/>.
+*/
+
+
+/*
+To generate:
+- Visit https://translate.google.com/?sl=auto&tl=en&op=translate
+- Open Language dropdown
+- Open Devtools and use the element picker to pick the root of the language picker
+- Right click on the element in devtools and click "Store as global variable"
+
+copy(Object.fromEntries(
+ Array.from(
+ temp1.querySelectorAll("[data-language-code]"),
+ e => [e.dataset.languageCode, e.children[1].textContent]
+ ).sort((a, b) => a[1] === "Detect language" ? -1 : b[1] === "Detect language" ? 1 : a[1].localeCompare(b[1]))
+))
+*/
+
+export type Language = keyof typeof Languages;
+
+export const Languages = {
+ "auto": "Detect language",
+ "af": "Afrikaans",
+ "sq": "Albanian",
+ "am": "Amharic",
+ "ar": "Arabic",
+ "hy": "Armenian",
+ "as": "Assamese",
+ "ay": "Aymara",
+ "az": "Azerbaijani",
+ "bm": "Bambara",
+ "eu": "Basque",
+ "be": "Belarusian",
+ "bn": "Bengali",
+ "bho": "Bhojpuri",
+ "bs": "Bosnian",
+ "bg": "Bulgarian",
+ "ca": "Catalan",
+ "ceb": "Cebuano",
+ "ny": "Chichewa",
+ "zh-CN": "Chinese (Simplified)",
+ "zh-TW": "Chinese (Traditional)",
+ "co": "Corsican",
+ "hr": "Croatian",
+ "cs": "Czech",
+ "da": "Danish",
+ "dv": "Dhivehi",
+ "doi": "Dogri",
+ "nl": "Dutch",
+ "en": "English",
+ "eo": "Esperanto",
+ "et": "Estonian",
+ "ee": "Ewe",
+ "tl": "Filipino",
+ "fi": "Finnish",
+ "fr": "French",
+ "fy": "Frisian",
+ "gl": "Galician",
+ "ka": "Georgian",
+ "de": "German",
+ "el": "Greek",
+ "gn": "Guarani",
+ "gu": "Gujarati",
+ "ht": "Haitian Creole",
+ "ha": "Hausa",
+ "haw": "Hawaiian",
+ "iw": "Hebrew",
+ "hi": "Hindi",
+ "hmn": "Hmong",
+ "hu": "Hungarian",
+ "is": "Icelandic",
+ "ig": "Igbo",
+ "ilo": "Ilocano",
+ "id": "Indonesian",
+ "ga": "Irish",
+ "it": "Italian",
+ "ja": "Japanese",
+ "jw": "Javanese",
+ "kn": "Kannada",
+ "kk": "Kazakh",
+ "km": "Khmer",
+ "rw": "Kinyarwanda",
+ "gom": "Konkani",
+ "ko": "Korean",
+ "kri": "Krio",
+ "ku": "Kurdish (Kurmanji)",
+ "ckb": "Kurdish (Sorani)",
+ "ky": "Kyrgyz",
+ "lo": "Lao",
+ "la": "Latin",
+ "lv": "Latvian",
+ "ln": "Lingala",
+ "lt": "Lithuanian",
+ "lg": "Luganda",
+ "lb": "Luxembourgish",
+ "mk": "Macedonian",
+ "mai": "Maithili",
+ "mg": "Malagasy",
+ "ms": "Malay",
+ "ml": "Malayalam",
+ "mt": "Maltese",
+ "mi": "Maori",
+ "mr": "Marathi",
+ "mni-Mtei": "Meiteilon (Manipuri)",
+ "lus": "Mizo",
+ "mn": "Mongolian",
+ "my": "Myanmar (Burmese)",
+ "ne": "Nepali",
+ "no": "Norwegian",
+ "or": "Odia (Oriya)",
+ "om": "Oromo",
+ "ps": "Pashto",
+ "fa": "Persian",
+ "pl": "Polish",
+ "pt": "Portuguese",
+ "pa": "Punjabi",
+ "qu": "Quechua",
+ "ro": "Romanian",
+ "ru": "Russian",
+ "sm": "Samoan",
+ "sa": "Sanskrit",
+ "gd": "Scots Gaelic",
+ "nso": "Sepedi",
+ "sr": "Serbian",
+ "st": "Sesotho",
+ "sn": "Shona",
+ "sd": "Sindhi",
+ "si": "Sinhala",
+ "sk": "Slovak",
+ "sl": "Slovenian",
+ "so": "Somali",
+ "es": "Spanish",
+ "su": "Sundanese",
+ "sw": "Swahili",
+ "sv": "Swedish",
+ "tg": "Tajik",
+ "ta": "Tamil",
+ "tt": "Tatar",
+ "te": "Telugu",
+ "th": "Thai",
+ "ti": "Tigrinya",
+ "ts": "Tsonga",
+ "tr": "Turkish",
+ "tk": "Turkmen",
+ "ak": "Twi",
+ "uk": "Ukrainian",
+ "ur": "Urdu",
+ "ug": "Uyghur",
+ "uz": "Uzbek",
+ "vi": "Vietnamese",
+ "cy": "Welsh",
+ "xh": "Xhosa",
+ "yi": "Yiddish",
+ "yo": "Yoruba",
+ "zu": "Zulu"
+} as const;
diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts
new file mode 100644
index 0000000..13e6540
--- /dev/null
+++ b/src/plugins/translate/settings.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { definePluginSettings } from "@api/Settings";
+import { OptionType } from "@utils/types";
+
+export const settings = definePluginSettings({
+ receivedInput: {
+ type: OptionType.STRING,
+ description: "Input language for received messages",
+ default: "auto",
+ hidden: true
+ },
+ receivedOutput: {
+ type: OptionType.STRING,
+ description: "Output language for received messages",
+ default: "en",
+ hidden: true
+ },
+ sentInput: {
+ type: OptionType.STRING,
+ description: "Input language for sent messages",
+ default: "auto",
+ hidden: true
+ },
+ sentOutput: {
+ type: OptionType.STRING,
+ description: "Output language for sent messages",
+ default: "en",
+ hidden: true
+ },
+ autoTranslate: {
+ type: OptionType.BOOLEAN,
+ description: "Automatically translate your messages before sending",
+ default: false
+ }
+});
diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css
new file mode 100644
index 0000000..b6d2223
--- /dev/null
+++ b/src/plugins/translate/styles.css
@@ -0,0 +1,37 @@
+.vc-trans-modal-content {
+ padding: 1em;
+}
+
+.vc-trans-modal-header {
+ justify-content: space-between;
+ align-content: center;
+}
+
+.vc-trans-modal-header h1 {
+ margin: 0;
+}
+
+.vc-trans-accessory {
+ color: var(--text-muted);
+ margin-top: 0.5em;
+ font-style: italic;
+ font-weight: 400;
+}
+
+.vc-trans-accessory svg {
+ margin-right: 0.25em;
+}
+
+.vc-trans-dismiss {
+ all: unset;
+ cursor: pointer;
+ color: var(--text-link);
+}
+
+.vc-trans-dismiss:is(:hover, :focus) {
+ text-decoration: underline;
+}
+
+.vc-trans-auto-translate {
+ color: var(--green-360);
+}
diff --git a/src/plugins/translate/utils.ts b/src/plugins/translate/utils.ts
new file mode 100644
index 0000000..784ec25
--- /dev/null
+++ b/src/plugins/translate/utils.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { settings } from "./settings";
+
+export const cl = classNameFactory("vc-trans-");
+
+interface TranslationData {
+ src: string;
+ sentences: {
+ // 🏳️‍⚧️
+ trans: string;
+ }[];
+}
+
+export interface TranslationValue {
+ src: 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"];
+
+ 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 }: TranslationData = await res.json();
+
+ return {
+ src,
+ text: sentences.
+ map(s => s?.trans).
+ filter(Boolean).
+ join("")
+ };
+}