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:
Prince527 2024-07-18 19:26:01 -03:00 committed by GitHub
parent 839af9d486
commit 80a9307139
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 260 additions and 467 deletions

View file

@ -18,19 +18,16 @@
import "./style.css";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addButton, removeButton } from "@api/MessagePopover";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common";
import { settings } from "./settings";
import { translate } from "./utils/misc";
import { TranslateChatBarIcon, TranslateIcon } from "./utils/TranslateIcon";
import { handleTranslate, TranslationAccessory } from "./utils/TranslationAccessory";
import { Accessory, handleTranslate } from "./utils/accessory";
import { Icon } from "./utils/icon";
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
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, (
<Menu.MenuItem
id="vc-trans"
id="ec-trans"
label="Translate"
icon={TranslateIcon}
action={async () => {
const trans = await translate("received", message.content);
handleTranslate(message.id, trans);
}}
icon={Icon}
action={() => handleTranslate(message)}
/>
));
};
export default definePlugin({
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],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
"message": messageCtxPatch
},
// not used, just here in case some other plugin wants it or w/e
translate,
start() {
addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
addAccessory("ec-translation", props => <Accessory message={props.message} />);
addChatBarButton("vc-translate", TranslateChatBarIcon);
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;
});
addButton("ec-translate", message => ({
label: "Translate",
icon: Icon,
message: message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: () => handleTranslate(message),
}));
},
stop() {
removePreSendListener(this.preSend);
removeChatBarButton("vc-translate");
removeButton("vc-translate");
removeAccessory("vc-translation");
},
removeButton("ec-translate");
removeAccessory("ec-translation");
}
});

View file

@ -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",
"af": "Afrikaans",
"sq": "Albanian",
@ -68,6 +68,7 @@ export const Languages = {
"ee": "Ewe",
"tl": "Filipino",
"tp": "Toki Pona",
"sh": "Shavian",
"fi": "Finnish",
"fr": "French",
"fy": "Frisian",

View 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;
};

View file

@ -1,69 +1,35 @@
/*
* 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/>.
*/
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
export const settings = definePluginSettings({
receivedInput: {
target: {
type: OptionType.STRING,
description: "Input language for received messages",
default: "auto",
hidden: true
},
receivedOutput: {
type: OptionType.STRING,
description: "Output language for received messages",
description: "Target language",
default: "en",
hidden: true
restartNeeded: 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: {
toki: {
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false
description: "Enable Toki Pona",
default: true,
restartNeeded: true
},
showChatBarButton: {
sitelen: {
type: OptionType.BOOLEAN,
description: "Show translate button in chat bar",
default: true
description: "Enable Sitelen Pona",
default: true,
restartNeeded: true
},
tokiPonaAPI: {
type: OptionType.STRING,
description: "An API to translate messages to Toki Pona",
default: "https://aiapi.serversmp.xyz/toki"
},
tokiPonaAuth: {
type: OptionType.STRING,
description: "An API key to use with the Toki Pona API",
default: ""
shavian: {
type: OptionType.BOOLEAN,
description: "Enable Shavian",
default: true,
restartNeeded: true
}
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
}>();
});

View file

@ -1,41 +1,20 @@
.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 {
.eq-trans-accessory {
color: var(--text-muted);
margin-top: 0.5em;
font-style: italic;
font-weight: 400;
}
.vc-trans-accessory svg {
.eq-trans-accessory svg {
margin-right: 0.25em;
}
.vc-trans-dismiss {
.eq-trans-dismiss {
all: unset;
cursor: pointer;
color: var(--text-link);
}
.vc-trans-dismiss:is(:hover, :focus) {
.eq-trans-dismiss:is(:hover, :focus) {
text-decoration: underline;
}
.vc-trans-auto-translate {
color: var(--green-360);
}
.vc-trans-chat-button {
scale: 1.085;
}
}

View file

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

View file

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

View file

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

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

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

View file

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

View 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;
}