New Plugin: Translate (#1089)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
parent
195f1a032f
commit
cb385d1b28
15 changed files with 669 additions and 5 deletions
|
@ -62,7 +62,7 @@
|
||||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||||
"arrow-parens": ["error", "as-needed"],
|
"arrow-parens": ["error", "as-needed"],
|
||||||
"eol-last": ["error", "always"],
|
"eol-last": ["error", "always"],
|
||||||
"func-call-spacing": ["error", "never"],
|
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||||
"no-multi-spaces": "error",
|
"no-multi-spaces": "error",
|
||||||
"no-trailing-spaces": "error",
|
"no-trailing-spaces": "error",
|
||||||
"no-whitespace-before-property": "error",
|
"no-whitespace-before-property": "error",
|
||||||
|
|
|
@ -129,6 +129,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||||
} else {
|
} else {
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||||
|
if (setting.hidden) return null;
|
||||||
|
|
||||||
function onChange(newValue: any) {
|
function onChange(newValue: any) {
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ function ChatBarIcon() {
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
innerClassName={ButtonWrapperClasses.button}
|
innerClassName={ButtonWrapperClasses.button}
|
||||||
onClick={() => buildEncModal()}
|
onClick={() => buildEncModal()}
|
||||||
style={{ padding: "0 4px" }}
|
style={{ padding: "0 2px", scale: "0.9" }}
|
||||||
>
|
>
|
||||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-st-button {
|
.vc-st-button {
|
||||||
padding: 0 8px;
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-st-button svg {
|
.vc-st-button svg {
|
||||||
|
|
|
@ -72,7 +72,7 @@ function SilentMessageToggle(chatBoxProps: {
|
||||||
size=""
|
size=""
|
||||||
look={ButtonLooks.BLANK}
|
look={ButtonLooks.BLANK}
|
||||||
innerClassName={ButtonWrapperClasses.button}
|
innerClassName={ButtonWrapperClasses.button}
|
||||||
style={{ padding: "0 8px" }}
|
style={{ padding: "0 6px" }}
|
||||||
>
|
>
|
||||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
@ -57,7 +57,7 @@ function SilentTypingToggle(chatBoxProps: {
|
||||||
size=""
|
size=""
|
||||||
look={ButtonLooks.BLANK}
|
look={ButtonLooks.BLANK}
|
||||||
innerClassName={ButtonWrapperClasses.button}
|
innerClassName={ButtonWrapperClasses.button}
|
||||||
style={{ padding: "0 8px" }}
|
style={{ padding: "0 6px" }}
|
||||||
>
|
>
|
||||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||||
|
|
70
src/plugins/translate/TranslateIcon.tsx
Normal file
70
src/plugins/translate/TranslateIcon.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
101
src/plugins/translate/TranslateModal.tsx
Normal file
101
src/plugins/translate/TranslateModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
62
src/plugins/translate/TranslationAccessory.tsx
Normal file
62
src/plugins/translate/TranslationAccessory.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
86
src/plugins/translate/index.tsx
Normal file
86
src/plugins/translate/index.tsx
Normal 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 }),
|
||||||
|
});
|
172
src/plugins/translate/languages.ts
Normal file
172
src/plugins/translate/languages.ts
Normal 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;
|
52
src/plugins/translate/settings.ts
Normal file
52
src/plugins/translate/settings.ts
Normal 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
|
||||||
|
}
|
||||||
|
});
|
37
src/plugins/translate/styles.css
Normal file
37
src/plugins/translate/styles.css
Normal 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);
|
||||||
|
}
|
75
src/plugins/translate/utils.ts
Normal file
75
src/plugins/translate/utils.ts
Normal 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("")
|
||||||
|
};
|
||||||
|
}
|
|
@ -147,8 +147,15 @@ export interface PluginSettingCommon {
|
||||||
description: string;
|
description: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange?(newValue: any): void;
|
onChange?(newValue: any): void;
|
||||||
|
/**
|
||||||
|
* Whether changing this setting requires a restart
|
||||||
|
*/
|
||||||
restartNeeded?: boolean;
|
restartNeeded?: boolean;
|
||||||
componentProps?: Record<string, any>;
|
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
|
* Set this if the setting only works on Browser or Desktop, not both
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue