New Plugin: Translate (#1089)

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
V 2023-05-10 23:14:04 +02:00 committed by GitHub
parent 195f1a032f
commit cb385d1b28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 669 additions and 5 deletions

View File

@ -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",

View File

@ -129,6 +129,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
} else {
const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.hidden) return null;
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}

View File

@ -85,7 +85,7 @@ function ChatBarIcon() {
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={() => buildEncModal()}
style={{ padding: "0 4px" }}
style={{ padding: "0 2px", scale: "0.9" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg

View File

@ -43,7 +43,7 @@
}
.vc-st-button {
padding: 0 8px;
padding: 0 6px;
}
.vc-st-button svg {

View File

@ -72,7 +72,7 @@ function SilentMessageToggle(chatBoxProps: {
size=""
look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button}
style={{ padding: "0 8px" }}
style={{ padding: "0 6px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg

View File

@ -57,7 +57,7 @@ function SilentTypingToggle(chatBoxProps: {
size=""
look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button}
style={{ padding: "0 8px" }}
style={{ padding: "0 6px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 }),
});

View File

@ -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;

View File

@ -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
}
});

View File

@ -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);
}

View File

@ -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("")
};
}

View File

@ -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<string, any>;
/**
* Hide this setting from the settings UI
*/
hidden?: boolean;
/**
* Set this if the setting only works on Browser or Desktop, not both
*/