Bitcointalk translator 4.1.1
// ==UserScript==
// @name Bitcointalk Translator
// @namespace https://bitcointalk.org/
// @version 4.1.1
// @description Bitcointalk translator 4.1.1
// @author GhostOfBitcoin
// @match https://bitcointalk.org/*
// @icon https://bitcointalk.org/favicon.ico
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect translate.googleapis.com
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
lang: GM_getValue('btc_lang', 'bn'),
side: GM_getValue('btc_side', false),
auto: GM_getValue('btc_auto', false),
panelOpen: false
};
function addStyle(css) {
if (typeof GM_addStyle === 'function') {
GM_addStyle(css);
return;
}
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
addStyle(`
:root{
--btc-main:#5b8cff;
--btc-second:#7f5cff;
}
.btc-floating-btn{
position:fixed;
width:52px;
height:52px;
border-radius:50%;
bottom:20px;
right:20px;
z-index:999999;
background:linear-gradient(135deg,var(--btc-main),var(--btc-second));
display:flex;
align-items:center;
justify-content:center;
cursor:grab;
box-shadow:0 10px 30px rgba(0,0,0,.35);
user-select:none;
transition:transform .15s ease,box-shadow .2s ease;
}
.btc-floating-btn:hover{
transform:scale(1.06);
}
.btc-floating-btn svg{
width:24px;
height:24px;
fill:white;
}
.btc-panel{
position:fixed;
width:290px;
background:linear-gradient(180deg,rgba(20,24,35,.98),rgba(15,18,28,.98));
backdrop-filter:blur(12px);
color:white;
border-radius:18px;
padding:18px;
z-index:999998;
box-shadow:0 20px 50px rgba(0,0,0,.45);
border:1px solid rgba(255,255,255,.06);
display:none;
animation:btcPanel .18s ease;
}
.btc-panel.active{
display:block;
}
@keyframes btcPanel{
from{opacity:0;transform:translateY(8px);}
to{opacity:1;transform:translateY(0);}
}
.btc-title{
font-size:16px;
font-weight:700;
margin-bottom:16px;
background:linear-gradient(90deg,#8ab4ff,#b388ff);
-webkit-background-clip:text;
-webkit-text-fill-color:transparent;
}
.btc-panel select{
width:100%;
padding:10px 12px;
border:none;
border-radius:12px;
background:#1d2330;
color:white;
outline:none;
margin-bottom:14px;
font-size:14px;
}
.btc-switch{
display:flex;
align-items:center;
gap:8px;
margin-bottom:14px;
font-size:14px;
}
.btc-thread-btn{
width:100%;
padding:11px;
border:none;
border-radius:12px;
background:linear-gradient(135deg,var(--btc-main),var(--btc-second));
color:white;
font-weight:700;
cursor:pointer;
transition:.2s ease;
}
.btc-thread-btn:hover{
transform:translateY(-1px);
}
.btc-translate-btn{
display:inline-flex;
align-items:center;
gap:5px;
padding:5px 10px;
margin-top:7px;
border-radius:8px;
background:rgba(91,140,255,.12);
border:1px solid rgba(91,140,255,.22);
color:#8ab4ff;
font-size:12px;
font-weight:700;
cursor:pointer;
transition:.15s ease;
}
.btc-translate-btn:hover{
background:rgba(91,140,255,.2);
}
.btc-translation{
margin-top:14px;
padding:16px;
border-radius:16px;
background:rgba(255,255,255,.04);
border:1px solid rgba(255,255,255,.06);
line-height:1.8;
font-size:14px;
animation:btcPanel .18s ease;
overflow-wrap:anywhere;
}
.btc-tools{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:12px;
}
.btc-tool-buttons{
display:flex;
gap:7px;
}
.btc-tool{
border:none;
padding:5px 10px;
border-radius:8px;
background:#222a38;
color:white;
cursor:pointer;
font-size:12px;
}
.btc-segment{
padding:12px 0;
border-top:1px solid rgba(255,255,255,.08);
}
.btc-segment:first-child{
padding-top:0;
border-top:none;
}
.btc-segment-label{
margin-bottom:6px;
color:#8ab4ff;
font-size:12px;
font-weight:700;
}
.btc-original{
margin-bottom:10px;
color:rgba(255,255,255,.78);
}
.btc-ar{
direction:rtl;
text-align:right;
font-family:Tahoma,Arial,sans-serif;
line-height:2.1;
}
.btc-selection-btn{
position:absolute;
z-index:1000000;
display:none;
padding:7px 11px;
border-radius:10px;
background:linear-gradient(135deg,var(--btc-main),var(--btc-second));
color:white;
box-shadow:0 10px 24px rgba(0,0,0,.32);
font-size:12px;
font-weight:700;
cursor:pointer;
user-select:none;
}
.btc-selection-popup{
position:absolute;
z-index:1000000;
width:min(360px,calc(100vw - 28px));
padding:14px;
border-radius:16px;
background:linear-gradient(180deg,rgba(20,24,35,.98),rgba(15,18,28,.98));
color:white;
border:1px solid rgba(255,255,255,.08);
box-shadow:0 20px 50px rgba(0,0,0,.45);
line-height:1.7;
font-size:14px;
overflow-wrap:anywhere;
}
@media(max-width:768px){
.btc-panel{
width:calc(100vw - 30px);
}
}
`);
class Cache {
constructor() {
this.prefix = 'btc_cache_';
}
hash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return hash;
}
get(key) {
return localStorage.getItem(this.prefix + key);
}
set(key, val) {
localStorage.setItem(this.prefix + key, val);
}
}
const cache = new Cache();
function escapeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
function normalizeText(text) {
return text
.replace(/\u00a0/g, ' ')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n[ \t]+/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function isIgnoredNode(node) {
if (node.nodeType !== Node.ELEMENT_NODE) {
return false;
}
return Boolean(
node.closest(
'.btc-translation,.btc-floating-btn,.btc-panel,.btc-selection-btn,.btc-selection-popup,script,style,noscript'
)
);
}
function getNodeText(node) {
const clone = node.cloneNode(true);
clone
.querySelectorAll('.btc-translation,.btc-floating-btn,.btc-panel,.btc-selection-btn,.btc-selection-popup,script,style,noscript')
.forEach(function (el) {
el.remove();
});
return normalizeText(clone.innerText || clone.textContent || '');
}
function pushSegment(segments, type, text) {
const clean = normalizeText(text);
if (!clean) {
return;
}
const previous = segments[segments.length - 1];
if (previous && previous.type === type && previous.text === clean) {
return;
}
segments.push({
type: type,
text: clean
});
}
function closestElement(node, selector) {
if (!node) {
return null;
}
const element =
node.nodeType === Node.ELEMENT_NODE
? node
: node.parentElement;
return element ? element.closest(selector) : null;
}
function extractPostSegments(content) {
const segments = [];
let replyParts = [];
function flushReply() {
pushSegment(segments, 'reply', replyParts.join('\n\n'));
replyParts = [];
}
function walk(node) {
if (isIgnoredNode(node)) {
return;
}
if (node.nodeType === Node.TEXT_NODE) {
const text = normalizeText(node.textContent || '');
if (text) {
replyParts.push(text);
}
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
if (node.matches('.quote,blockquote')) {
flushReply();
pushSegment(segments, 'quote', getNodeText(node));
return;
}
if (node.matches('br')) {
replyParts.push('\n');
return;
}
Array.from(node.childNodes).forEach(walk);
if (node.matches('p,div,li,ul,ol,table,tr')) {
replyParts.push('\n');
}
}
Array.from(content.childNodes).forEach(walk);
flushReply();
if (!segments.length) {
pushSegment(segments, 'reply', getNodeText(content));
}
return segments;
}
async function translateText(text, target) {
if (!text) return text;
const key = cache.hash(text + target);
const cached = cache.get(key);
if (cached) {
return cached;
}
try {
const url =
`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${target}&dt=t&dt=bd&dj=1&q=${encodeURIComponent(text)}`;
const res = await fetch(url);
const data = await res.json();
let translated = '';
if (data.sentences) {
data.sentences.forEach(function (s) {
translated += s.trans || '';
});
}
translated = translated.trim();
if (!translated) {
translated = text;
}
cache.set(key, translated);
return translated;
} catch (e) {
console.error(e);
return text;
}
}
async function translateSegments(segments) {
const translated = [];
for (const segment of segments) {
translated.push({
type: segment.type,
text: segment.text,
translated: await translateText(segment.text, CONFIG.lang)
});
}
return translated;
}
function createTranslationBox(segments) {
const box = document.createElement('div');
const allTranslated = segments.map(function (segment) {
return segment.translated;
}).join('\n\n');
let quoteCount = 0;
box.className = 'btc-translation';
if (CONFIG.lang === 'ar') {
box.classList.add('btc-ar');
}
box.innerHTML = `
<div class="btc-tools">
<strong>Translation</strong>
<div class="btc-tool-buttons">
<button class="btc-tool btc-copy">Copy</button>
<button class="btc-tool btc-speak">Speak</button>
<button class="btc-tool btc-close">Close</button>
</div>
</div>
<div class="btc-segments">
${segments.map(function (segment) {
const label = segment.type === 'quote' ? `Quote ${++quoteCount}` : 'User text';
return `
<div class="btc-segment btc-segment-${segment.type}">
<div class="btc-segment-label">${label}</div>
${
CONFIG.side
? `
<div class="btc-original">
<b>Original:</b>
<div>${escapeHTML(segment.text)}</div>
</div>
<div>
<b>Translated:</b>
<div>${escapeHTML(segment.translated)}</div>
</div>
`
: `<div>${escapeHTML(segment.translated)}</div>`
}
</div>
`;
}).join('')}
</div>
`;
box.querySelector('.btc-copy').onclick = function () {
navigator.clipboard.writeText(allTranslated);
};
box.querySelector('.btc-speak').onclick = function () {
const speech = new SpeechSynthesisUtterance(allTranslated);
speech.lang = CONFIG.lang;
speechSynthesis.speak(speech);
};
box.querySelector('.btc-close').onclick = function () {
box.remove();
};
return box;
}
async function processPost(post) {
if (post.dataset.btcDone) {
return;
}
post.dataset.btcDone = '1';
const footer = post.querySelector('.smalltext');
const content = post.querySelector('.post');
if (!footer || !content) {
return;
}
const btn = document.createElement('div');
btn.className = 'btc-translate-btn';
btn.innerHTML = 'Translate';
footer.appendChild(btn);
btn.onclick = async function () {
const old = post.querySelector('.btc-translation');
if (old) {
old.remove();
return;
}
const segments = extractPostSegments(content);
if (!segments.length) {
return;
}
btn.innerHTML = 'Translating';
const translatedSegments = await translateSegments(segments);
const box = createTranslationBox(translatedSegments);
content.appendChild(box);
btn.innerHTML = 'Done';
setTimeout(function () {
btn.innerHTML = 'Translate';
}, 1200);
};
if (CONFIG.auto) {
btn.click();
}
}
function observe() {
const observer = new MutationObserver(function () {
const posts = document.querySelectorAll('td.windowbg,td.windowbg2');
posts.forEach(processPost);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const posts = document.querySelectorAll('td.windowbg,td.windowbg2');
posts.forEach(processPost);
}
function createFloatingUI() {
const btn = document.createElement('div');
btn.className = 'btc-floating-btn';
btn.innerHTML = `
<svg viewBox="0 0 24 24">
<path d="M12 2a10 10 0 100 20 10 10 0 000-20zm6.93 9h-3.02a15.7 15.7 0 00-1.32-5.01A8.02 8.02 0 0118.93 11zm-6.93 9c-.83-1.2-1.53-3.02-1.86-5h3.72c-.33 1.98-1.03 3.8-1.86 5zm-2.14-7a13.7 13.7 0 010-2h4.28a13.7 13.7 0 010 2H9.86zm.28 2h3.72c-.33 1.98-1.03 3.8-1.86 5-.83-1.2-1.53-3.02-1.86-5zm-4.07-4a8.02 8.02 0 014.34-5.01A15.7 15.7 0 009.09 11H6.07zm0 2h3.02c.12 1.78.56 3.5 1.32 5.01A8.02 8.02 0 016.07 13zm8.84 5.01c.76-1.51 1.2-3.23 1.32-5.01h3.02a8.02 8.02 0 01-4.34 5.01z"/>
</svg>
`;
document.body.appendChild(btn);
const panel = document.createElement('div');
panel.className = 'btc-panel';
panel.innerHTML = `
<div class="btc-title">Bitcointalk Translator</div>
<select id="btc-lang">
<option value="ar">العربية (Arabic)</option>
<option value="id">Indonesian</option>
<option value="es">Spanish</option>
<option value="zh-CN">Chinese</option>
<option value="hr">Croatian</option>
<option value="de">German</option>
<option value="el">Greek</option>
<option value="he">Hebrew</option>
<option value="fr">Français</option>
<option value="hi">Hindi</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="nl">Nederlands</option>
<option value="yo">Yoruba</option>
<option value="ko">Korean</option>
<option value="tl">Pilipinas</option>
<option value="pt">Portuguese</option>
<option value="ru">Russian</option>
<option value="ro">Romanian</option>
<option value="sv">Swedish</option>
<option value="tr">Turkish</option>
<option value="ur">Urdu</option>
<option value="bn">বাংলা</option>
</select>
<label class="btc-switch">
<input type="checkbox" id="btc-side" ${CONFIG.side ? 'checked' : ''}>
Side by Side
</label>
<label class="btc-switch">
<input type="checkbox" id="btc-auto" ${CONFIG.auto ? 'checked' : ''}>
Auto Translate
</label>
<button class="btc-thread-btn">Translate Entire Thread</button>
`;
document.body.appendChild(panel);
function updatePanelPosition() {
const rect = btn.getBoundingClientRect();
const left = Math.max(10, Math.min(window.innerWidth - panel.offsetWidth - 10, rect.left - 230));
const top = Math.max(10, Math.min(window.innerHeight - panel.offsetHeight - 10, rect.top - 10));
panel.style.left = left + 'px';
panel.style.top = top + 'px';
}
updatePanelPosition();
btn.addEventListener('click', function () {
if (btn.dragging) {
return;
}
CONFIG.panelOpen = !CONFIG.panelOpen;
if (CONFIG.panelOpen) {
updatePanelPosition();
panel.classList.add('active');
} else {
panel.classList.remove('active');
}
});
let dragging = false;
let offsetX = 0;
let offsetY = 0;
btn.addEventListener('mousedown', function (e) {
dragging = true;
btn.dragging = false;
offsetX = e.clientX - btn.offsetLeft;
offsetY = e.clientY - btn.offsetTop;
panel.classList.remove('active');
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', function (e) {
if (!dragging) {
return;
}
btn.dragging = true;
btn.style.left = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, e.clientX - offsetX)) + 'px';
btn.style.top = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, e.clientY - offsetY)) + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
});
document.addEventListener('mouseup', function () {
dragging = false;
document.body.style.userSelect = '';
});
const lang = panel.querySelector('#btc-lang');
lang.value = CONFIG.lang;
lang.onchange = function (e) {
CONFIG.lang = e.target.value;
GM_setValue('btc_lang', e.target.value);
};
panel.querySelector('#btc-side').onchange = function (e) {
CONFIG.side = e.target.checked;
GM_setValue('btc_side', e.target.checked);
};
panel.querySelector('#btc-auto').onchange = function (e) {
CONFIG.auto = e.target.checked;
GM_setValue('btc_auto', e.target.checked);
};
panel.querySelector('.btc-thread-btn').onclick = async function () {
const buttons = document.querySelectorAll('.btc-translate-btn');
for (const b of buttons) {
const post = b.closest('td');
if (post && !post.querySelector('.btc-translation')) {
b.click();
await new Promise(function (resolve) {
setTimeout(resolve, 300);
});
}
}
};
}
function createSelectionTranslator() {
const button = document.createElement('div');
button.className = 'btc-selection-btn';
button.textContent = 'Translate selection';
document.body.appendChild(button);
let selectedText = '';
function removePopup() {
document
.querySelectorAll('.btc-selection-popup')
.forEach(function (el) {
el.remove();
});
}
function hideButton() {
button.style.display = 'none';
}
document.addEventListener('mouseup', function () {
setTimeout(function () {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) {
hideButton();
return;
}
if (closestElement(selection.anchorNode, '.btc-panel,.btc-floating-btn,.btc-selection-btn,.btc-selection-popup')) {
hideButton();
return;
}
selectedText = normalizeText(selection.toString());
if (!selectedText) {
hideButton();
return;
}
const rect = selection.getRangeAt(0).getBoundingClientRect();
button.style.left = Math.min(window.scrollX + rect.left, window.scrollX + window.innerWidth - 150) + 'px';
button.style.top = (window.scrollY + rect.bottom + 8) + 'px';
button.style.display = 'block';
}, 0);
});
document.addEventListener('mousedown', function (e) {
if (!closestElement(e.target, '.btc-selection-btn,.btc-selection-popup')) {
hideButton();
}
});
button.addEventListener('mousedown', function (e) {
e.preventDefault();
});
button.addEventListener('click', async function () {
if (!selectedText) {
return;
}
removePopup();
button.textContent = 'Translating';
const translated = await translateText(selectedText, CONFIG.lang);
const popup = document.createElement('div');
popup.className = 'btc-selection-popup';
if (CONFIG.lang === 'ar') {
popup.classList.add('btc-ar');
}
popup.style.left = button.style.left;
popup.style.top = (parseFloat(button.style.top) + 34) + 'px';
popup.innerHTML = `
<div class="btc-tools">
<strong>Selection</strong>
<div class="btc-tool-buttons">
<button class="btc-tool btc-copy">Copy</button>
<button class="btc-tool btc-close">Close</button>
</div>
</div>
<div>${escapeHTML(translated)}</div>
`;
popup.querySelector('.btc-copy').onclick = function () {
navigator.clipboard.writeText(translated);
};
popup.querySelector('.btc-close').onclick = function () {
popup.remove();
};
document.body.appendChild(popup);
button.textContent = 'Translate selection';
hideButton();
});
}
document.addEventListener('keydown', function (e) {
if (e.altKey && e.key === 't') {
const btn = document.querySelector('.btc-translate-btn');
if (btn) {
btn.click();
}
}
});
function init() {
if (!document.body) {
setTimeout(init, 100);
return;
}
observe();
createFloatingUI();
createSelectionTranslator();
console.log('Bitcointalk Translator V4.1.1 Loaded');
}
init();
})();