From cb385d1b280551eb16f1f9836a93cc9bcc43da15 Mon Sep 17 00:00:00 2001 From: V Date: Wed, 10 May 2023 23:14:04 +0200 Subject: [PATCH] New Plugin: Translate (#1089) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- .eslintrc.json | 2 +- src/components/PluginSettings/PluginModal.tsx | 2 + src/plugins/invisibleChat/index.tsx | 2 +- src/plugins/sendTimestamps/styles.css | 2 +- src/plugins/silentMessageToggle.tsx | 2 +- src/plugins/silentTyping.tsx | 2 +- src/plugins/translate/TranslateIcon.tsx | 70 +++++++ src/plugins/translate/TranslateModal.tsx | 101 ++++++++++ .../translate/TranslationAccessory.tsx | 62 +++++++ src/plugins/translate/index.tsx | 86 +++++++++ src/plugins/translate/languages.ts | 172 ++++++++++++++++++ src/plugins/translate/settings.ts | 52 ++++++ src/plugins/translate/styles.css | 37 ++++ src/plugins/translate/utils.ts | 75 ++++++++ src/utils/types.ts | 7 + 15 files changed, 669 insertions(+), 5 deletions(-) create mode 100644 src/plugins/translate/TranslateIcon.tsx create mode 100644 src/plugins/translate/TranslateModal.tsx create mode 100644 src/plugins/translate/TranslationAccessory.tsx create mode 100644 src/plugins/translate/index.tsx create mode 100644 src/plugins/translate/languages.ts create mode 100644 src/plugins/translate/settings.ts create mode 100644 src/plugins/translate/styles.css create mode 100644 src/plugins/translate/utils.ts diff --git a/.eslintrc.json b/.eslintrc.json index aaaaaeb6..e45e446f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -62,7 +62,7 @@ "indent": ["error", 4, { "SwitchCase": 1 }], "arrow-parens": ["error", "as-needed"], "eol-last": ["error", "always"], - "func-call-spacing": ["error", "never"], + "@typescript-eslint/func-call-spacing": ["error", "never"], "no-multi-spaces": "error", "no-trailing-spaces": "error", "no-whitespace-before-property": "error", diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index c5028732..1818a8bc 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -129,6 +129,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti return There are no settings for this plugin.; } else { const options = Object.entries(plugin.options).map(([key, setting]) => { + if (setting.hidden) return null; + function onChange(newValue: any) { setTempSettings(s => ({ ...s, [key]: newValue })); } diff --git a/src/plugins/invisibleChat/index.tsx b/src/plugins/invisibleChat/index.tsx index eb92d764..feae548f 100644 --- a/src/plugins/invisibleChat/index.tsx +++ b/src/plugins/invisibleChat/index.tsx @@ -85,7 +85,7 @@ function ChatBarIcon() { onMouseLeave={onMouseLeave} innerClassName={ButtonWrapperClasses.button} onClick={() => buildEncModal()} - style={{ padding: "0 4px" }} + style={{ padding: "0 2px", scale: "0.9" }} >
diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx new file mode 100644 index 00000000..d944ec16 --- /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 . +*/ + +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 ( + + + + ); +} + +export function TranslateChatBarIcon() { + const { autoTranslate } = settings.use(["autoTranslate"]); + + return ( + + {({ onMouseEnter, onMouseLeave }) => ( +
+ +
+ )} + + ); +} diff --git a/src/plugins/translate/TranslateModal.tsx b/src/plugins/translate/TranslateModal.tsx new file mode 100644 index 00000000..7628a31e --- /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 . +*/ + +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 ( +
+ + {settings.def[settingsKey].description} + + + o.value === currentValue)} + placeholder={"Select a language"} + maxVisibleItems={5} + closeOnSelect={true} + onChange={v => settings.store[settingsKey] = v} + /> +
+ ); +} + +function AutoTranslateToggle() { + const value = settings.use(["autoTranslate"]).autoTranslate; + + return ( + settings.store.autoTranslate = v} + note={settings.def.autoTranslate.description} + hideBorder + > + Auto Translate + + ); +} + + +export function TranslateModal({ rootProps }: { rootProps: ModalProps; }) { + return ( + + + + Translate + + + + + + {LanguageSettingKeys.map(s => ( + + ))} + + + + + + + ); +} diff --git a/src/plugins/translate/TranslationAccessory.tsx b/src/plugins/translate/TranslationAccessory.tsx new file mode 100644 index 00000000..4f46e4b3 --- /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 . +*/ + +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 void>(); + +export function handleTranslate(messageId: string, data: TranslationValue) { + TranslationSetters.get(messageId)!(data); +} + +function Dismiss({ onDismiss }: { onDismiss: () => void; }) { + return ( + + ); +} + +export function TranslationAccessory({ message }: { message: Message; }) { + const [translation, setTranslation] = useState(); + + useEffect(() => { + TranslationSetters.set(message.id, setTranslation); + + return () => void TranslationSetters.delete(message.id); + }, []); + + if (!translation) return null; + + return ( + + + {Parser.parse(translation.text)} + {" "} + (translated from {Languages[translation.src] ?? translation.src} - setTranslation(undefined)} />) + + ); +} diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx new file mode 100644 index 00000000..cb612547 --- /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 . +*/ + +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 => ); + + 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 00000000..c3be0535 --- /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 . +*/ + + +/* +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 00000000..13e6540a --- /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 . +*/ + +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 00000000..b6d2223a --- /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 00000000..784ec25f --- /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 . +*/ + +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 { + 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("") + }; +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 60dc4d4c..5c7a85cf 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -147,8 +147,15 @@ export interface PluginSettingCommon { description: string; placeholder?: string; onChange?(newValue: any): void; + /** + * Whether changing this setting requires a restart + */ restartNeeded?: boolean; componentProps?: Record; + /** + * Hide this setting from the settings UI + */ + hidden?: boolean; /** * Set this if the setting only works on Browser or Desktop, not both */