From 692ee9a1df80980b3fb2f6c9d08d5925661b3348 Mon Sep 17 00:00:00 2001 From: TomFront Date: Wed, 8 Jul 2026 19:12:02 +0200 Subject: [PATCH] Localize profile badge and gate injection on plugin state - Add per-locale labels for the Partnered Server Owner profile badge via LocaleStore, covering Discord's supported languages with English fallback (en-US/en-GB/fr confirmed, the rest best-effort). - Gate all injection behind an `enabled` flag so the settings panel can no longer inject while the plugin is toggled off. - On stop(), fully undo injections across every surface (header, server-profile card, invite embed, profile badge) via removeAllInjections(). Co-Authored-By: Claude Opus 4.8 --- ServerBadges/index.tsx | 115 +++++++++++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 17 deletions(-) diff --git a/ServerBadges/index.tsx b/ServerBadges/index.tsx index 94bf873..09f8891 100644 --- a/ServerBadges/index.tsx +++ b/ServerBadges/index.tsx @@ -38,6 +38,7 @@ import { GuildStore, React, UserStore } from "@webpack/common"; const UserProfileStore = findStoreLazy("UserProfileStore"); const GuildProfileStore = findStoreLazy("GuildProfileStore"); const InviteStore = findStoreLazy("InviteStore"); +const LocaleStore = findStoreLazy("LocaleStore"); type Override = "none" | "verified" | "partner" | "both"; @@ -59,6 +60,54 @@ const PARTNER_BADGE = { const OUR_MARK = "__vcServerBadges"; +// Discord localizes "Partnered Server Owner" server-side for real badges, so the +// client doesn't ship the string — we supply our own per-locale labels and fall +// back to English. Keys are Discord locale codes (LocaleStore.locale). +// +// CONFIRMED as Discord's exact official wording: en-US, en-GB, fr. +// All others are best-effort translations and MAY differ from Discord's official +// phrasing — verify against a real badge in that language and correct as needed. +const PARTNER_LABELS: Record = { + "en-US": "Partnered Server Owner", + "en-GB": "Partnered Server Owner", + "fr": "Propriétaire d'un serveur partenaire", + // — best-effort below — + "da": "Ejer af en partnerserver", + "de": "Inhaber eines Partnerservers", + "es-ES": "Propietario de un servidor asociado", + "es-419": "Propietario de un servidor asociado", + "hr": "Vlasnik partnerskog poslužitelja", + "it": "Proprietario di un server partner", + "lt": "Partnerio serverio savininkas", + "hu": "Partnerszerver tulajdonosa", + "nl": "Eigenaar van een partnerserver", + "no": "Eier av en partnerserver", + "pl": "Właściciel serwera partnerskiego", + "pt-BR": "Proprietário de servidor parceiro", + "ro": "Proprietar al unui server partener", + "fi": "Kumppanipalvelimen omistaja", + "sv-SE": "Ägare av en partnerserver", + "vi": "Chủ sở hữu máy chủ đối tác", + "tr": "Partner Sunucu Sahibi", + "cs": "Vlastník partnerského serveru", + "el": "Ιδιοκτήτης συνεργαζόμενου διακομιστή", + "bg": "Собственик на партньорски сървър", + "ru": "Владелец партнёрского сервера", + "uk": "Власник партнерського сервера", + "hi": "पार्टनर सर्वर के मालिक", + "th": "เจ้าของเซิร์ฟเวอร์พาร์ทเนอร์", + "zh-CN": "合作服务器所有者", + "ja": "パートナーサーバーのオーナー", + "zh-TW": "合作夥伴伺服器擁有者", + "ko": "파트너 서버 소유자" +}; + +function partnerLabel(): string { + const loc: string = LocaleStore?.locale ?? LocaleStore?.getLocale?.() ?? "en-US"; + // exact locale ("es-ES") -> language prefix ("es") -> English + return PARTNER_LABELS[loc] ?? PARTNER_LABELS[loc.split("-")[0]] ?? PARTNER_LABELS["en-US"]; +} + // ─── Owned-servers settings panel ──────────────────────────────────────────── const OVERRIDE_OPTIONS: { value: Override; label: string; }[] = [ @@ -161,6 +210,11 @@ const settings = definePluginSettings({ // remove a server's genuine features. const injectedFeatures = new Map>(); +// Whether the plugin is currently enabled. Only start()/stop() flip this, and +// all injection is gated on it — so interacting with the settings panel while +// the plugin is toggled off can't sneak injections back in. +let enabled = false; + function ownedOverriddenGuilds() { const me = UserStore.getCurrentUser(); const guilds = GuildStore.getGuilds() as Record; @@ -222,6 +276,14 @@ function addArrayFeatures(obj: any, features: string[]) { } } +// Inverse of addArrayFeatures — used on cleanup. +function removeArrayFeatures(obj: any, features: string[]) { + if (!obj || !Array.isArray(obj.features)) return; + if (features.some(f => obj.features.includes(f))) { + try { obj.features = obj.features.filter((f: string) => !features.includes(f)); } catch { /* not writable */ } + } +} + // The "server profile" card (server-settings preview) reads GuildProfileStore, // which has its OWN plain-array `features` — separate from the GuildStore record. function syncGuildProfiles() { @@ -269,12 +331,13 @@ function syncProfileBadge() { if (!profile || !Array.isArray(profile.badges)) return; const badges: any[] = profile.badges; + const label = partnerLabel(); const hasReal = badges.some(b => b?.id === PARTNER_BADGE.id && !b?.[OUR_MARK]); - const hasOurs = badges.some(b => b?.[OUR_MARK]); + const ours = badges.find(b => b?.[OUR_MARK]); const want = settings.store.profileBadge && ownsPartneredServer() && !hasReal; - if (want && hasOurs) return; // already added - if (!want && !hasOurs) return; // nothing to remove + if (want && ours?.description === label) return; // already added with the right label + if (!want && !ours) return; // nothing to remove const cleaned = badges.filter(b => !b?.[OUR_MARK]); @@ -283,19 +346,44 @@ function syncProfileBadge() { // Insert after any leading Nitro/premium badges. let idx = 0; while (idx < cleaned.length && String(cleaned[idx]?.id).startsWith("premium")) idx++; - next = [...cleaned.slice(0, idx), { ...PARTNER_BADGE, [OUR_MARK]: true }, ...cleaned.slice(idx)]; + next = [...cleaned.slice(0, idx), { ...PARTNER_BADGE, description: label, [OUR_MARK]: true }, ...cleaned.slice(idx)]; } try { profile.badges = next; } catch { /* badges not writable in this build */ } } function sync() { + if (!enabled) return; // never inject while the plugin is off syncGuildFeatures(); syncGuildProfiles(); syncInvites(); syncProfileBadge(); } +// Undo every injection across all surfaces, using injectedFeatures as the record +// of exactly what we added (so genuine features are never touched). +function removeAllInjections() { + for (const [id, mine] of injectedFeatures) { + const feats = [...mine]; + const guild = GuildStore.getGuild(id); + for (const f of feats) guild?.features?.delete?.(f); + + removeArrayFeatures(GuildProfileStore?.getProfile?.(id), feats); + + const code = InviteStore?.getInviteKeyForGuildId?.(id); + const inv = code && InviteStore?.getInvite?.(code); + removeArrayFeatures(inv?.guild, feats); + removeArrayFeatures(inv?.profile, feats); + } + injectedFeatures.clear(); + + const me = UserStore.getCurrentUser(); + const profile = me && UserProfileStore?.getUserProfile?.(me.id); + if (profile && Array.isArray(profile.badges) && profile.badges.some((b: any) => b?.[OUR_MARK])) { + try { profile.badges = profile.badges.filter((b: any) => !b?.[OUR_MARK]); } catch { /* ignore */ } + } +} + // ─── Lifecycle ──────────────────────────────────────────────────────────────── export default definePlugin({ @@ -305,6 +393,7 @@ export default definePlugin({ settings, start() { + enabled = true; sync(); // Re-apply whenever any of the relevant data is (re)loaded or updated, so // late-loading guilds/profiles still get patched. We mutate/replace objects @@ -314,27 +403,19 @@ export default definePlugin({ UserProfileStore?.addChangeListener?.(sync); GuildProfileStore?.addChangeListener?.(sync); InviteStore?.addChangeListener?.(sync); + LocaleStore?.addChangeListener?.(sync); }, stop() { + enabled = false; + GuildStore.removeChangeListener(sync); UserStore.removeChangeListener(sync); UserProfileStore?.removeChangeListener?.(sync); GuildProfileStore?.removeChangeListener?.(sync); InviteStore?.removeChangeListener?.(sync); + LocaleStore?.removeChangeListener?.(sync); - // Remove the guild features we injected. - for (const [id, mine] of injectedFeatures) { - const guild = GuildStore.getGuild(id); - for (const f of mine) guild?.features?.delete?.(f); - } - injectedFeatures.clear(); - - // Remove the profile badge we injected. - const me = UserStore.getCurrentUser(); - const profile = me && UserProfileStore?.getUserProfile?.(me.id); - if (profile && Array.isArray(profile.badges) && profile.badges.some((b: any) => b?.[OUR_MARK])) { - try { profile.badges = profile.badges.filter((b: any) => !b?.[OUR_MARK]); } catch { /* ignore */ } - } + removeAllInjections(); } });