remake messageTranslate, fix gifcollections bug (#967)
* remake message translations plugin, fix only show gifs for gifcollections * Update index.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
785bf059e9
commit
61341888d8
6 changed files with 275 additions and 45 deletions
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import "./style.css";
|
||||
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ export default definePlugin({
|
|||
oldTrendingCat = instance.props.trendingCategories;
|
||||
}
|
||||
if (settings.store.onlyShowCollections) {
|
||||
instance.props.trendingCategories = [...this.sortedCollections(), ...instance.props.favorites];
|
||||
instance.props.trendingCategories = this.sortedCollections();
|
||||
} else if (oldTrendingCat != null) {
|
||||
instance.props.trendingCategories = [...this.sortedCollections(), ...oldTrendingCat];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,64 +4,111 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import "./styles.css";
|
||||
|
||||
const settings = definePluginSettings(
|
||||
{
|
||||
targetLanguage: {
|
||||
type: OptionType.STRING,
|
||||
description: "The language messages should be translated to",
|
||||
default: "en",
|
||||
restartNeeded: true
|
||||
},
|
||||
confidenceRequirement: {
|
||||
type: OptionType.STRING,
|
||||
description: "The confidence required to translated the message. Best not to edit unless you know what you're doing",
|
||||
default: "0.8",
|
||||
restartNeeded: true
|
||||
},
|
||||
});
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore, FluxDispatcher, MessageStore, UserStore } from "@webpack/common";
|
||||
|
||||
async function translateAPI(sourceLang: string, targetLang: string, text: string): Promise<any> {
|
||||
import { getIgnoredChannels, getIgnoredGuilds, getIgnoredUsers, settings } from "./settings";
|
||||
import { MessageWithContent } from "./types";
|
||||
import { clearCache, getCached, hasFailed, isInProgress, translate } from "./utils/translate";
|
||||
|
||||
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dj=1&q=${encodeURIComponent(text)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to translate "${text}" from ${sourceLang} to ${targetLang}: ${response.status} ${response.statusText}`);
|
||||
const cl = classNameFactory("mt-");
|
||||
const translatedMessages = new Map<string, string>();
|
||||
|
||||
function shouldTranslate(message: MessageWithContent): boolean {
|
||||
if (!message.content || typeof message.content !== "string") return false;
|
||||
if (hasFailed(message.id, message.content)) return false;
|
||||
|
||||
if (settings.store.skipOwnMessages) {
|
||||
const currentUserId = UserStore.getCurrentUser()?.id;
|
||||
if (currentUserId && message.author?.id === currentUserId) return false;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
if (settings.store.skipBotMessages && message.author?.bot) return false;
|
||||
|
||||
if (message.author && getIgnoredUsers().has(message.author.id)) return false;
|
||||
if (message.channel_id && getIgnoredChannels().has(message.channel_id)) return false;
|
||||
|
||||
const guildId = message.channel_id ? ChannelStore.getChannel(message.channel_id)?.guild_id : null;
|
||||
if (guildId && getIgnoredGuilds().has(guildId)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function TranslateMessage(string) {
|
||||
// there may be a better way to do this lmao
|
||||
if (string.includes("(Translated)")) return string;
|
||||
function triggerReRender(message: MessageWithContent) {
|
||||
const current = MessageStore.getMessage(message.channel_id, message.id);
|
||||
if (!current) return;
|
||||
|
||||
const response = await translateAPI("auto", settings.store.targetLanguage, string);
|
||||
|
||||
if (response.src === settings.store.targetLanguage || response.confidence < settings.store.confidenceRequirement) return string;
|
||||
|
||||
const { sentences }: { sentences: { trans?: string; }[]; } = await response;
|
||||
const translatedText = sentences.map(s => s?.trans).filter(Boolean).join("");
|
||||
|
||||
return `${translatedText} *(Translated)*`;
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_UPDATE",
|
||||
message: current,
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageTranslate",
|
||||
description: "Auto translate messages to your language",
|
||||
authors: [Devs.Samwich],
|
||||
description: "Auto translate messages to your language with caching, per-channel toggles, and more options.",
|
||||
authors: [EquicordDevs.creations],
|
||||
settings,
|
||||
TranslateMessage: TranslateMessage,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '.CUSTOM_GIFT?""',
|
||||
replacement: {
|
||||
match: /renderContentOnly:\i}=\i;/,
|
||||
replace: "$&$self.TranslateMessage(arguments[0].message.content).then(response => arguments[0].message.content = response);"
|
||||
}
|
||||
replacement: [
|
||||
{
|
||||
match: /message:(\i),message:\{id:\i\}.{0,200}renderContentOnly:\i\}=\i;/,
|
||||
replace: "$&$1=$self.transformMessage($1);",
|
||||
},
|
||||
{
|
||||
match: /childrenMessageContent:(\i),/g,
|
||||
replace: "childrenMessageContent:$self.wrapContent($1,arguments[0].message.id),",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
|
||||
transformMessage(message: MessageWithContent): MessageWithContent {
|
||||
if (!settings.store.autoTranslate || !shouldTranslate(message)) {
|
||||
translatedMessages.delete(message.id);
|
||||
return message;
|
||||
}
|
||||
|
||||
const cached = getCached(message.id);
|
||||
if (cached) {
|
||||
if (cached.original !== message.content) {
|
||||
clearCache(message.id);
|
||||
translatedMessages.delete(message.id);
|
||||
return message;
|
||||
}
|
||||
translatedMessages.set(message.id, cached.sourceLang);
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(message)), message, {
|
||||
content: cached.translated,
|
||||
}) as MessageWithContent;
|
||||
}
|
||||
|
||||
translatedMessages.delete(message.id);
|
||||
if (!isInProgress(message.id)) {
|
||||
translate(message.id, message.content).then(result => {
|
||||
if (result) triggerReRender(message);
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
},
|
||||
|
||||
wrapContent(content: any, messageId: string) {
|
||||
const sourceLang = translatedMessages.get(messageId);
|
||||
if (!sourceLang) return content;
|
||||
return (
|
||||
<>
|
||||
{content}
|
||||
{settings.store.showIndicator && (
|
||||
<div className={cl("indicator")}>translated from {sourceLang}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
72
src/equicordplugins/messageTranslate/settings.ts
Normal file
72
src/equicordplugins/messageTranslate/settings.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 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({
|
||||
targetLanguage: {
|
||||
type: OptionType.STRING,
|
||||
description: "Target language code for translations (e.g. en, es, fr, de, ja).",
|
||||
default: "en",
|
||||
},
|
||||
confidenceRequirement: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Minimum confidence (0 to 1) required to show a translation.",
|
||||
default: 0.8,
|
||||
},
|
||||
autoTranslate: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically translate messages as they appear.",
|
||||
default: true,
|
||||
},
|
||||
skipOwnMessages: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Do not translate your own messages.",
|
||||
default: true,
|
||||
},
|
||||
skipBotMessages: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Do not translate bot messages.",
|
||||
default: false,
|
||||
},
|
||||
ignoredGuilds: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma-separated list of server IDs to not translate in.",
|
||||
default: "",
|
||||
},
|
||||
ignoredChannels: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma-separated list of channel IDs to not translate in.",
|
||||
default: "",
|
||||
},
|
||||
ignoredUsers: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma-separated list of user IDs to not translate.",
|
||||
default: "",
|
||||
},
|
||||
showIndicator: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Append a small (translated) indicator to translated messages.",
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
function parseIdList(value: string): Set<string> {
|
||||
return new Set(value.split(",").map(s => s.trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
export function getIgnoredGuilds(): Set<string> {
|
||||
return parseIdList(settings.store.ignoredGuilds);
|
||||
}
|
||||
|
||||
export function getIgnoredChannels(): Set<string> {
|
||||
return parseIdList(settings.store.ignoredChannels);
|
||||
}
|
||||
|
||||
export function getIgnoredUsers(): Set<string> {
|
||||
return parseIdList(settings.store.ignoredUsers);
|
||||
}
|
||||
9
src/equicordplugins/messageTranslate/styles.css
Normal file
9
src/equicordplugins/messageTranslate/styles.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.mt-indicator {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
margin-top: 0.15em;
|
||||
opacity: 0.75;
|
||||
}
|
||||
21
src/equicordplugins/messageTranslate/types.ts
Normal file
21
src/equicordplugins/messageTranslate/types.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Message } from "@vencord/discord-types";
|
||||
|
||||
export interface TranslateResponse {
|
||||
src: string;
|
||||
confidence: number;
|
||||
sentences: { trans?: string; }[];
|
||||
}
|
||||
|
||||
export interface CachedTranslation {
|
||||
original: string;
|
||||
translated: string;
|
||||
sourceLang: string;
|
||||
}
|
||||
|
||||
export type MessageWithContent = Message & { content: string; };
|
||||
81
src/equicordplugins/messageTranslate/utils/translate.ts
Normal file
81
src/equicordplugins/messageTranslate/utils/translate.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
import { settings } from "../settings";
|
||||
import { CachedTranslation, TranslateResponse } from "../types";
|
||||
|
||||
const logger = new Logger("MessageTranslate");
|
||||
|
||||
const translationCache = new Map<string, CachedTranslation>();
|
||||
const inProgress = new Set<string>();
|
||||
const failed = new Map<string, string>();
|
||||
|
||||
export function getCached(messageId: string): CachedTranslation | undefined {
|
||||
return translationCache.get(messageId);
|
||||
}
|
||||
|
||||
export function hasFailed(messageId: string, text: string): boolean {
|
||||
return failed.get(messageId) === text;
|
||||
}
|
||||
|
||||
export function isInProgress(messageId: string): boolean {
|
||||
return inProgress.has(messageId);
|
||||
}
|
||||
|
||||
export function clearCache(messageId: string) {
|
||||
translationCache.delete(messageId);
|
||||
failed.delete(messageId);
|
||||
}
|
||||
|
||||
async function fetchTranslation(text: string, targetLang: string): Promise<TranslateResponse> {
|
||||
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&dj=1&q=${encodeURIComponent(text)}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation API returned ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function translate(messageId: string, text: string): Promise<CachedTranslation | null> {
|
||||
if (inProgress.has(messageId)) return null;
|
||||
if (translationCache.has(messageId)) return translationCache.get(messageId)!;
|
||||
|
||||
inProgress.add(messageId);
|
||||
|
||||
try {
|
||||
const targetLang = settings.store.targetLanguage;
|
||||
const response = await fetchTranslation(text, targetLang);
|
||||
|
||||
if (response.src === targetLang || response.confidence < settings.store.confidenceRequirement) {
|
||||
failed.set(messageId, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
const translatedText = response.sentences.map(s => s.trans).filter(Boolean).join("");
|
||||
if (!translatedText || translatedText === text) {
|
||||
failed.set(messageId, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry: CachedTranslation = {
|
||||
original: text,
|
||||
translated: translatedText,
|
||||
sourceLang: response.src,
|
||||
};
|
||||
translationCache.set(messageId, entry);
|
||||
return entry;
|
||||
} catch (e) {
|
||||
logger.error("Translation failed", e);
|
||||
failed.set(messageId, text);
|
||||
return null;
|
||||
} finally {
|
||||
inProgress.delete(messageId);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue