Scramble tweets post-Babel style while keeping the art visible
// ==UserScript== // @name Twitter De-Translate // @namespace http://greasyfork.org/ // @version 1.0 // @description Scramble tweets post-Babel style while keeping the art visible // @author Nova DasSarma <[email protected]> // @license MIT // @match https://twitter.com/* // @match https://x.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-end // ==/UserScript== (function() { 'use strict'; // Storage key for detranslated users const STORAGE_KEY = 'detranslated_users'; // Get list of detranslated users from storage function getDetranslatedUsers() { const stored = GM_getValue(STORAGE_KEY, '[]'); return new Set(JSON.parse(stored)); } // Save list of detranslated users to storage function saveDetranslatedUsers(users) { GM_setValue(STORAGE_KEY, JSON.stringify([...users])); } // Babel-style scrambling function function babelScramble(text) { const babel = ['ꝁ', 'ꝃ', 'ꝅ', 'ꝇ', 'ꝉ', 'ꝋ', 'ꝍ', 'ꝏ', 'ꝑ', 'ꝓ', 'ꝕ', 'ꝗ', 'ꝙ', 'ꝛ', 'ꝝ', 'ꝟ', 'ꝡ', 'ꝣ', 'ꝥ', 'ꝧ', 'ა', 'ბ', 'გ', 'დ', 'ე', 'ვ', 'ზ', 'თ', 'ი', 'კ', '㍿', '㌀', '㌁', '㌂', '㌃', '㌄', '㌅', '㌆', '㌇', '㌈']; return text.split('').map(char => { // Match any Unicode letter or CJK character if (char.match(/[\p{L}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u)) { return babel[Math.floor(Math.random() * babel.length)]; } return char; }).join(''); } // Add CSS for the button function addStyles() { const style = document.createElement('style'); style.textContent = ` .detranslate-btn { display: inline-flex; align-items: center; gap: 4px; padding: 0px; border-radius: 0px; font-size: 13px; font-weight: 400; cursor: pointer; transition: all 0.2s; border: none; margin-left: 4px; vertical-align: middle; background-color: transparent; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .detranslate-btn:not(.active) { color: rgb(29, 155, 240); } .detranslate-btn:not(.active):hover { text-decoration: underline; } .detranslate-btn.active { color: rgb(147, 51, 234); } .detranslate-btn.active:hover { text-decoration: underline; } .detranslated-text { user-select: none; } `; document.head.appendChild(style); } // Extract username from tweet article element function getUsernameFromTweet(tweetElement) { // Try multiple selectors for username const usernameSelectors = [ 'a[href*="/"]:not([href*="/status/"])[role="link"]', '[data-testid="User-Name"] a[role="link"]', 'a[dir="ltr"][role="link"]' ]; for (const selector of usernameSelectors) { const links = tweetElement.querySelectorAll(selector); for (const link of links) { const href = link.getAttribute('href'); if (href && href.startsWith('/') && !href.includes('/status/') && !href.includes('/photo/')) { const username = href.split('/')[1]; if (username && username.length > 0 && username !== 'home' && username !== 'explore') { return '@' + username; } } } } return null; } // Create detranslate button function createButton(username, isActive) { const btn = document.createElement('button'); btn.className = 'detranslate-btn' + (isActive ? ' active' : ''); btn.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="m5 8 6 6M4 14l6-6 2-3M2 5h12M15 16l-4 4M11 13h10M20 20l-1-1"/> </svg> ${isActive ? 'Re-translate' : 'De-translate'} `; return btn; } // Process a single tweet function processTweet(tweetElement) { // Skip if already processed if (tweetElement.hasAttribute('data-detranslate-processed')) { return; } tweetElement.setAttribute('data-detranslate-processed', 'true'); const username = getUsernameFromTweet(tweetElement); if (!username) return; const detranslatedUsers = getDetranslatedUsers(); const isDetranslated = detranslatedUsers.has(username); // Find the tweet text element const textElement = tweetElement.querySelector('[data-testid="tweetText"]'); if (!textElement) return; // Store original text if not already stored if (!textElement.hasAttribute('data-original-text')) { textElement.setAttribute('data-original-text', textElement.textContent); } // Apply detranslation if needed if (isDetranslated) { const originalText = textElement.getAttribute('data-original-text'); textElement.textContent = babelScramble(originalText); textElement.classList.add('detranslated-text'); } // Find where to insert button - look for Grok translation bar or tweet text parent let insertLocation = null; // First try to find the Grok auto-translate section const grokTranslation = tweetElement.querySelector('[aria-label*="Show original"]'); if (grokTranslation) { // Insert next to "Show original" button insertLocation = grokTranslation.parentElement; } else { // Fallback: insert in the header near the username insertLocation = tweetElement.querySelector('[data-testid="User-Name"]'); } if (!insertLocation || insertLocation.querySelector('.detranslate-btn')) return; // Create and insert button const btn = createButton(username, isDetranslated); btn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const detranslatedUsers = getDetranslatedUsers(); const wasDetranslated = detranslatedUsers.has(username); if (wasDetranslated) { detranslatedUsers.delete(username); } else { detranslatedUsers.add(username); } saveDetranslatedUsers(detranslatedUsers); // Refresh all tweets from this user reprocessAllTweets(); }); insertLocation.appendChild(btn); } // Reprocess all tweets (for when toggle changes) function reprocessAllTweets() { const detranslatedUsers = getDetranslatedUsers(); // Remove processed flag and reset all tweets document.querySelectorAll('[data-detranslate-processed]').forEach(el => { el.removeAttribute('data-detranslate-processed'); // Reset text to original const textElement = el.querySelector('[data-testid="tweetText"]'); if (textElement && textElement.hasAttribute('data-original-text')) { const originalText = textElement.getAttribute('data-original-text'); textElement.textContent = originalText; textElement.classList.remove('detranslated-text'); } // Remove existing buttons el.querySelectorAll('.detranslate-btn').forEach(btn => btn.remove()); }); // Reprocess all visible tweets const tweets = document.querySelectorAll('article[data-testid="tweet"]'); tweets.forEach(processTweet); } // Observe for new tweets function observeTimeline() { const observer = new MutationObserver((mutations) => { const tweets = document.querySelectorAll('article[data-testid="tweet"]'); tweets.forEach(processTweet); }); observer.observe(document.body, { childList: true, subtree: true }); } // Initialize addStyles(); observeTimeline(); // Process initial tweets after a short delay setTimeout(() => { const tweets = document.querySelectorAll('article[data-testid="tweet"]'); tweets.forEach(processTweet); }, 1000); })();