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:
creations 2026-04-11 22:55:17 -04:00 • committed by GitHub
parent 785bf059e9
commit 61341888d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 275 additions and 45 deletions

View file

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

View file

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

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

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

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

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