mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-14 00:53:04 -04:00
Update Translate+ (#13)
* Updated Translate+ I came up with a way to keep Shavian * Updated Translate+ Removed a setting because its not needed, I made my API public :) * Update languages.ts * Update index.tsx * Translator+ v2 * Added types * Update languages.ts
This commit is contained in:
parent
839af9d486
commit
80a9307139
12 changed files with 260 additions and 467 deletions
|
@ -18,19 +18,16 @@
|
||||||
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
|
||||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||||
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
|
||||||
import { addButton, removeButton } from "@api/MessagePopover";
|
import { addButton, removeButton } from "@api/MessagePopover";
|
||||||
import { Devs, EquicordDevs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { ChannelStore, Menu } from "@webpack/common";
|
import { ChannelStore, Menu } from "@webpack/common";
|
||||||
|
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { translate } from "./utils/misc";
|
import { Accessory, handleTranslate } from "./utils/accessory";
|
||||||
import { TranslateChatBarIcon, TranslateIcon } from "./utils/TranslateIcon";
|
import { Icon } from "./utils/icon";
|
||||||
import { handleTranslate, TranslationAccessory } from "./utils/TranslationAccessory";
|
|
||||||
|
|
||||||
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
|
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
|
||||||
if (!message.content) return;
|
if (!message.content) return;
|
||||||
|
@ -40,61 +37,37 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) =>
|
||||||
|
|
||||||
group.splice(group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, (
|
group.splice(group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, (
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="vc-trans"
|
id="ec-trans"
|
||||||
label="Translate"
|
label="Translate"
|
||||||
icon={TranslateIcon}
|
icon={Icon}
|
||||||
action={async () => {
|
action={() => handleTranslate(message)}
|
||||||
const trans = await translate("received", message.content);
|
|
||||||
handleTranslate(message.id, trans);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Translate+",
|
name: "Translate+",
|
||||||
description: "Translate messages with Google Translate and Toki Pona AI",
|
description: "Vencord's translate plugin but with support for artistic languages!",
|
||||||
|
dependencies: ["MessageAccessoriesAPI"],
|
||||||
authors: [Devs.Ven, EquicordDevs.Prince527],
|
authors: [Devs.Ven, EquicordDevs.Prince527],
|
||||||
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
|
|
||||||
settings,
|
settings,
|
||||||
contextMenus: {
|
contextMenus: {
|
||||||
"message": messageCtxPatch
|
"message": messageCtxPatch
|
||||||
},
|
},
|
||||||
// not used, just here in case some other plugin wants it or w/e
|
|
||||||
translate,
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
|
addAccessory("ec-translation", props => <Accessory message={props.message} />);
|
||||||
|
|
||||||
addChatBarButton("vc-translate", TranslateChatBarIcon);
|
addButton("ec-translate", message => ({
|
||||||
|
label: "Translate",
|
||||||
addButton("vc-translate", message => {
|
icon: Icon,
|
||||||
if (!message.content) return null;
|
message: message,
|
||||||
|
channel: ChannelStore.getChannel(message.channel_id),
|
||||||
return {
|
onClick: () => handleTranslate(message),
|
||||||
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() {
|
stop() {
|
||||||
removePreSendListener(this.preSend);
|
removeButton("ec-translate");
|
||||||
removeChatBarButton("vc-translate");
|
removeAccessory("ec-translation");
|
||||||
removeButton("vc-translate");
|
}
|
||||||
removeAccessory("vc-translation");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,9 +31,9 @@ copy(Object.fromEntries(
|
||||||
))
|
))
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type Language = keyof typeof Languages;
|
export type languages = keyof typeof languages;
|
||||||
|
|
||||||
export const Languages = {
|
export const languages = {
|
||||||
"auto": "Detect language",
|
"auto": "Detect language",
|
||||||
"af": "Afrikaans",
|
"af": "Afrikaans",
|
||||||
"sq": "Albanian",
|
"sq": "Albanian",
|
||||||
|
@ -68,6 +68,7 @@ export const Languages = {
|
||||||
"ee": "Ewe",
|
"ee": "Ewe",
|
||||||
"tl": "Filipino",
|
"tl": "Filipino",
|
||||||
"tp": "Toki Pona",
|
"tp": "Toki Pona",
|
||||||
|
"sh": "Shavian",
|
||||||
"fi": "Finnish",
|
"fi": "Finnish",
|
||||||
"fr": "French",
|
"fr": "French",
|
||||||
"fy": "Frisian",
|
"fy": "Frisian",
|
||||||
|
|
19
src/equicordplugins/translatePlus/misc/types.ts
Normal file
19
src/equicordplugins/translatePlus/misc/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
|
||||||
|
export const cl = classNameFactory("eq-trans-");
|
||||||
|
|
||||||
|
export interface Translation {
|
||||||
|
text: string;
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IconProps = {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
|
@ -1,69 +1,35 @@
|
||||||
/*
|
/*
|
||||||
* Vencord, a modification for Discord's desktop app
|
* Vencord, a Discord client mod
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
*
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
* 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 { definePluginSettings } from "@api/Settings";
|
||||||
import { OptionType } from "@utils/types";
|
import { OptionType } from "@utils/types";
|
||||||
|
|
||||||
export const settings = definePluginSettings({
|
export const settings = definePluginSettings({
|
||||||
receivedInput: {
|
target: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Input language for received messages",
|
description: "Target language",
|
||||||
default: "auto",
|
|
||||||
hidden: true
|
|
||||||
},
|
|
||||||
receivedOutput: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Output language for received messages",
|
|
||||||
default: "en",
|
default: "en",
|
||||||
hidden: true
|
restartNeeded: true
|
||||||
},
|
},
|
||||||
sentInput: {
|
toki: {
|
||||||
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,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
description: "Enable Toki Pona",
|
||||||
default: false
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
},
|
},
|
||||||
showChatBarButton: {
|
sitelen: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Show translate button in chat bar",
|
description: "Enable Sitelen Pona",
|
||||||
default: true
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
},
|
},
|
||||||
tokiPonaAPI: {
|
shavian: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.BOOLEAN,
|
||||||
description: "An API to translate messages to Toki Pona",
|
description: "Enable Shavian",
|
||||||
default: "https://aiapi.serversmp.xyz/toki"
|
default: true,
|
||||||
},
|
restartNeeded: true
|
||||||
tokiPonaAuth: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "An API key to use with the Toki Pona API",
|
|
||||||
default: ""
|
|
||||||
}
|
}
|
||||||
}).withPrivateSettings<{
|
});
|
||||||
showAutoTranslateAlert: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
|
@ -1,41 +1,20 @@
|
||||||
.vc-trans-modal-content {
|
.eq-trans-accessory {
|
||||||
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);
|
color: var(--text-muted);
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-trans-accessory svg {
|
.eq-trans-accessory svg {
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-trans-dismiss {
|
.eq-trans-dismiss {
|
||||||
all: unset;
|
all: unset;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-link);
|
color: var(--text-link);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-trans-dismiss:is(:hover, :focus) {
|
.eq-trans-dismiss:is(:hover, :focus) {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-trans-auto-translate {
|
|
||||||
color: var(--green-360);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-trans-chat-button {
|
|
||||||
scale: 1.085;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 { ChatBarButton } from "@api/ChatButtons";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { openModal } from "@utils/modal";
|
|
||||||
import { Alerts, Forms } from "@webpack/common";
|
|
||||||
|
|
||||||
import { settings } from "../settings";
|
|
||||||
import { cl } from "./misc";
|
|
||||||
import { TranslateModal } from "./TranslateModal";
|
|
||||||
|
|
||||||
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 const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
|
||||||
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
|
|
||||||
|
|
||||||
if (!isMainChat || !showChatBarButton) return null;
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
const newState = !autoTranslate;
|
|
||||||
settings.store.autoTranslate = newState;
|
|
||||||
if (newState && settings.store.showAutoTranslateAlert !== false)
|
|
||||||
Alerts.show({
|
|
||||||
title: "Vencord Auto-Translate Enabled",
|
|
||||||
body: <>
|
|
||||||
<Forms.FormText>
|
|
||||||
You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent.
|
|
||||||
</Forms.FormText>
|
|
||||||
<Forms.FormText className={Margins.top16}>
|
|
||||||
If this was an accident, disable it again, or it will change your message content before sending.
|
|
||||||
</Forms.FormText>
|
|
||||||
</>,
|
|
||||||
cancelText: "Disable Auto-Translate",
|
|
||||||
confirmText: "Got it",
|
|
||||||
secondaryConfirmText: "Don't show again",
|
|
||||||
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
|
|
||||||
onCancel: () => settings.store.autoTranslate = false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChatBarButton
|
|
||||||
tooltip="Open Translate Modal"
|
|
||||||
onClick={e => {
|
|
||||||
if (e.shiftKey) return toggle();
|
|
||||||
|
|
||||||
openModal(props => (
|
|
||||||
<TranslateModal rootProps={props} />
|
|
||||||
));
|
|
||||||
}}
|
|
||||||
onContextMenu={() => toggle()}
|
|
||||||
buttonProps={{
|
|
||||||
"aria-haspopup": "dialog"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TranslateIcon className={cl({ "auto-translate": autoTranslate, "chat-button": true })} />
|
|
||||||
</ChatBarButton>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 "../misc/languages";
|
|
||||||
import { settings } from "../settings";
|
|
||||||
import { cl } from "./misc";
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 "../misc/languages";
|
|
||||||
import { cl, TranslationValue } from "./misc";
|
|
||||||
import { TranslateIcon } from "./TranslateIcon";
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
// Ignore MessageLinkEmbeds messages
|
|
||||||
if ((message as any).vencordEmbeddedBy) return;
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
42
src/equicordplugins/translatePlus/utils/accessory.tsx
Normal file
42
src/equicordplugins/translatePlus/utils/accessory.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Parser, useEffect, useState } from "@webpack/common";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
import { languages } from "../misc/languages";
|
||||||
|
import { cl, Translation } from "../misc/types";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
import { translate } from "./translator";
|
||||||
|
|
||||||
|
const setters = new Map();
|
||||||
|
|
||||||
|
export function Accessory({ message }: { message: Message; }) {
|
||||||
|
const [translation, setTranslation] = useState<Translation | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((message as any).vencordEmbeddedBy) return;
|
||||||
|
|
||||||
|
setters.set(message.id, setTranslation);
|
||||||
|
|
||||||
|
return () => void setters.delete(message.id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!translation) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cl("accessory")}>
|
||||||
|
<Icon width={16} height={16} />
|
||||||
|
{Parser.parse(translation.text)}
|
||||||
|
{" "}
|
||||||
|
(translated from {languages[translation.src] ?? translation.src} - <button onClick={() => setTranslation(undefined)} className={cl("dismiss")}>Dismiss</button>)
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleTranslate(message: Message) {
|
||||||
|
setters.get(message.id)!(await translate(message.content));
|
||||||
|
}
|
17
src/equicordplugins/translatePlus/utils/icon.tsx
Normal file
17
src/equicordplugins/translatePlus/utils/icon.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cl, IconProps } from "../misc/types";
|
||||||
|
|
||||||
|
export function Icon({ width = 24, height = 24 }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 96 960 960" height={width} width={height} className={cl("icon")}>
|
||||||
|
<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" />
|
||||||
|
<path fill="currentColor" d="m 830.17456,136.43701 c -11.54729,0 -20.84473,8.71252 -20.84473,19.53369 v 66.21826 h -66.21826 c -10.82122,0 -19.53369,9.29373 -19.53369,20.84107 0,11.54734 8.71247,20.84473 19.53369,20.84473 h 66.21826 v 66.21826 c 0,10.8212 9.29742,19.53369 20.84473,19.53369 11.54731,0 20.84106,-8.71249 20.84106,-19.53369 v -66.21826 h 66.21827 c 10.82124,0 19.53369,-9.29737 19.53369,-20.84473 0,-11.54736 -8.71245,-20.84107 -19.53369,-20.84107 H 851.01562 V 155.9707 c 0,-10.82117 -9.29377,-19.53369 -20.84106,-19.53369 z" />
|
||||||
|
<rect fill="currentColor" width="0.42110577" height="2.1055288" x="848.52814" y="112.42313" ry="0.2105529" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dictonary = /\b(?:leko|weka|pan|lete|linja|lipu|suli|nimi|akesi|misikeke|selo|ike|sijelo|sona|lili|pimeja|ante|jo|loje|telo|walo|kijetesantakalu|kasi|waso|wile|utala|lukin|sina|lape|ma|pilin|jasima|la|olin|pipi|meso|lawa|pi|pakala|oko|tan|ken|jaki|unpa|esun|seme|sitelen|len|kule|soko|open|ala|tenpo|lon|sinpin|pini|kokosila|mama|musi|monsi|mewika|taso|ona|mun|kiwen|tomo|mute|mi|nena|palisa|meli|laso|wawa|ale|kipisi|kulupu|ilo|lupa|nanpa|en|mu|jelo|kili|tonsi|moku|ni|kama|pu|poki|monsuta|sin|lasina|poka|soweli|sewi|elena|epiku|moli|pona|lanpan|alasa|anu|kute|uta|luka|suno|sama|awen|namako|suwi|noka|seli|mije|sike|jan|pali|tawa|inli|nasa|mani|wan|insa|nijon|nasin|kalama|ijo|toki|anpa|kala|kepeken|ko|kon|pana|tu|supa|kin|usawi|yupekosi)\b/gm;
|
|
||||||
|
|
||||||
function isTokiPona(string) {
|
|
||||||
const words = string.split(/\s+/);
|
|
||||||
|
|
||||||
const matches = string.match(dictonary) || [];
|
|
||||||
|
|
||||||
const percentage = (matches.length / words.length) * 100;
|
|
||||||
|
|
||||||
return percentage >= 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> {
|
|
||||||
let output;
|
|
||||||
|
|
||||||
if (isTokiPona(text)) {
|
|
||||||
const [api, auth] = [settings.store.tokiPonaAPI, settings.store.tokiPonaAuth];
|
|
||||||
|
|
||||||
const translate = await (await fetch(api, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Basic ${auth}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
text: text,
|
|
||||||
src: "tl",
|
|
||||||
target: "en"
|
|
||||||
})
|
|
||||||
})).json();
|
|
||||||
|
|
||||||
output = {
|
|
||||||
src: "tp",
|
|
||||||
text: translate.translation[0]
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const [sourceLang, targetLang] = [settings.store[kind + "Input"], settings.store[kind + "Output"]];
|
|
||||||
|
|
||||||
const translate = await (await fetch(`https://translate.googleapis.com/translate_a/single?${new URLSearchParams({ client: "gtx", sl: sourceLang, tl: targetLang, dt: "t", dj: "1", source: "input", q: text })}`)).json();
|
|
||||||
|
|
||||||
output = {
|
|
||||||
src: translate.src,
|
|
||||||
text: translate.sentences.map(s => s.trans).filter(Boolean).join("")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
135
src/equicordplugins/translatePlus/utils/translator.ts
Normal file
135
src/equicordplugins/translatePlus/utils/translator.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { settings } from "../settings";
|
||||||
|
|
||||||
|
function isTokiPona(text: string) {
|
||||||
|
const dictionary = /\b(?:leko|weka|pan|lete|linja|lipu|suli|nimi|akesi|misikeke|selo|ike|sijelo|sona|lili|pimeja|ante|jo|loje|telo|walo|kijetesantakalu|kasi|waso|wile|utala|lukin|sina|lape|ma|pilin|jasima|la|olin|pipi|meso|lawa|pi|pakala|oko|tan|ken|jaki|unpa|esun|seme|sitelen|len|kule|soko|open|ala|tenpo|lon|sinpin|pini|kokosila|mama|musi|monsi|mewika|taso|ona|mun|kiwen|tomo|mute|mi|nena|palisa|meli|laso|wawa|ale|kipisi|kulupu|ilo|lupa|nanpa|en|mu|jelo|kili|tonsi|moku|ni|kama|pu|poki|monsuta|sin|lasina|poka|soweli|sewi|elena|epiku|moli|pona|lanpan|alasa|anu|kute|uta|luka|suno|sama|awen|namako|suwi|noka|seli|mije|sike|jan|pali|tawa|inli|nasa|mani|wan|insa|nijon|nasin|kalama|ijo|toki|anpa|kala|kepeken|ko|kon|pana|tu|supa|kin|usawi|yupekosi)\b/gm;
|
||||||
|
|
||||||
|
return (text.match(dictionary) || []).length >= text.split(/\s+/).length * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSitelen(text: string) {
|
||||||
|
const dictionary = /(?:||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||)/gm;
|
||||||
|
|
||||||
|
return dictionary.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShavian(text: string) {
|
||||||
|
const shavianRegex = /[\u{10450}-\u{1047F}]+/u;
|
||||||
|
|
||||||
|
return shavianRegex.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translateShavian(message: string) {
|
||||||
|
const dictionary = await (await fetch("https://forkprince.github.io/TranslatePlus/shavian.json")).json();
|
||||||
|
|
||||||
|
const punctuationMap = {
|
||||||
|
'"': "\"",
|
||||||
|
"«": "\"",
|
||||||
|
"»": "\"",
|
||||||
|
",": ",",
|
||||||
|
"!": "!",
|
||||||
|
"?": "?",
|
||||||
|
".": ".",
|
||||||
|
"(": "(",
|
||||||
|
")": ")",
|
||||||
|
"/": "/",
|
||||||
|
";": ";",
|
||||||
|
":": ":"
|
||||||
|
};
|
||||||
|
|
||||||
|
let translated = "";
|
||||||
|
const words = message.split(/\s+/);
|
||||||
|
|
||||||
|
for (let word of words) {
|
||||||
|
let punctuationBefore = "", punctuationAfter = "";
|
||||||
|
|
||||||
|
if (word[0] in punctuationMap) {
|
||||||
|
punctuationBefore = punctuationMap[word[0]];
|
||||||
|
word = word.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (word[word.length - 1] in punctuationMap) {
|
||||||
|
punctuationAfter = punctuationMap[word[word.length - 1]];
|
||||||
|
word = word.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
translated += punctuationBefore;
|
||||||
|
|
||||||
|
if (word in dictionary) translated += dictionary[word];
|
||||||
|
else translated += word;
|
||||||
|
|
||||||
|
translated += punctuationAfter + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return translated.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translateSitelen(message: string) {
|
||||||
|
message = Array.from(message).join(" ");
|
||||||
|
|
||||||
|
const dictionary = await (await fetch("https://forkprince.github.io/TranslatePlus/sitelen-pona.json")).json();
|
||||||
|
|
||||||
|
const sorted = Object.keys(dictionary).sort((a, b) => b.length - a.length);
|
||||||
|
|
||||||
|
const pattern = new RegExp(`(${sorted.join("|")})`, "g");
|
||||||
|
|
||||||
|
const translate = message.replace(pattern, match => dictionary[match]);
|
||||||
|
|
||||||
|
return translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function google(target: string, text: string) {
|
||||||
|
const translate = await (await fetch(`https://translate.googleapis.com/translate_a/single?${new URLSearchParams({ client: "gtx", sl: "auto", tl: target, dt: "t", dj: "1", source: "input", q: text })}`)).json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
src: translate.src,
|
||||||
|
text: translate.sentences.map(s => s.trans).filter(Boolean).join("")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function translate(text: string): Promise<any> {
|
||||||
|
const { target, toki, sitelen, shavian } = settings.store;
|
||||||
|
|
||||||
|
const output = { src: "", text: "" };
|
||||||
|
|
||||||
|
if ((isTokiPona(text) || isSitelen(text)) && (toki || sitelen)) {
|
||||||
|
if (isSitelen(text) && sitelen) text = await translateSitelen(text);
|
||||||
|
|
||||||
|
console.log(text);
|
||||||
|
|
||||||
|
const translate = await (await fetch("https://aiapi.serversmp.xyz/toki", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
src: "tl",
|
||||||
|
target: "en"
|
||||||
|
})
|
||||||
|
})).json();
|
||||||
|
|
||||||
|
console.log(translate);
|
||||||
|
|
||||||
|
output.src = "tp";
|
||||||
|
output.text = target === "en" ? translate.translation[0] : (await google(target, translate.translation[0])).text;
|
||||||
|
} else if (isShavian(text) && shavian) {
|
||||||
|
const translate = await translateShavian(text);
|
||||||
|
|
||||||
|
output.src = "sh";
|
||||||
|
output.text = target === "en" ? translate : (await google(target, translate)).text;
|
||||||
|
} else {
|
||||||
|
const translate = await google(target, text);
|
||||||
|
|
||||||
|
output.src = translate.src;
|
||||||
|
output.text = translate.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue