From 61341888d825a195afdecedf0b5c4245b72aaf92 Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 11 Apr 2026 22:55:17 -0400 Subject: [PATCH] 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> --- src/equicordplugins/gifCollections/index.tsx | 4 +- .../messageTranslate/index.tsx | 133 ++++++++++++------ .../messageTranslate/settings.ts | 72 ++++++++++ .../messageTranslate/styles.css | 9 ++ src/equicordplugins/messageTranslate/types.ts | 21 +++ .../messageTranslate/utils/translate.ts | 81 +++++++++++ 6 files changed, 275 insertions(+), 45 deletions(-) create mode 100644 src/equicordplugins/messageTranslate/settings.ts create mode 100644 src/equicordplugins/messageTranslate/styles.css create mode 100644 src/equicordplugins/messageTranslate/types.ts create mode 100644 src/equicordplugins/messageTranslate/utils/translate.ts diff --git a/src/equicordplugins/gifCollections/index.tsx b/src/equicordplugins/gifCollections/index.tsx index 54fb969a0..8bfc180d7 100644 --- a/src/equicordplugins/gifCollections/index.tsx +++ b/src/equicordplugins/gifCollections/index.tsx @@ -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]; } diff --git a/src/equicordplugins/messageTranslate/index.tsx b/src/equicordplugins/messageTranslate/index.tsx index 6384b6ee9..68adbbd0b 100644 --- a/src/equicordplugins/messageTranslate/index.tsx +++ b/src/equicordplugins/messageTranslate/index.tsx @@ -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 { +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(); + +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 && ( +
translated from {sourceLang}
+ )} + + ); + }, }); diff --git a/src/equicordplugins/messageTranslate/settings.ts b/src/equicordplugins/messageTranslate/settings.ts new file mode 100644 index 000000000..491240737 --- /dev/null +++ b/src/equicordplugins/messageTranslate/settings.ts @@ -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 { + return new Set(value.split(",").map(s => s.trim()).filter(Boolean)); +} + +export function getIgnoredGuilds(): Set { + return parseIdList(settings.store.ignoredGuilds); +} + +export function getIgnoredChannels(): Set { + return parseIdList(settings.store.ignoredChannels); +} + +export function getIgnoredUsers(): Set { + return parseIdList(settings.store.ignoredUsers); +} diff --git a/src/equicordplugins/messageTranslate/styles.css b/src/equicordplugins/messageTranslate/styles.css new file mode 100644 index 000000000..14cd820e6 --- /dev/null +++ b/src/equicordplugins/messageTranslate/styles.css @@ -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; +} diff --git a/src/equicordplugins/messageTranslate/types.ts b/src/equicordplugins/messageTranslate/types.ts new file mode 100644 index 000000000..d6cb8f1f5 --- /dev/null +++ b/src/equicordplugins/messageTranslate/types.ts @@ -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; }; diff --git a/src/equicordplugins/messageTranslate/utils/translate.ts b/src/equicordplugins/messageTranslate/utils/translate.ts new file mode 100644 index 000000000..612b26c1a --- /dev/null +++ b/src/equicordplugins/messageTranslate/utils/translate.ts @@ -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(); +const inProgress = new Set(); +const failed = new Map(); + +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 { + 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 { + 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); + } +}