Bitcointalk Translator with modern UI and multi-language support
// ==UserScript==
// @name Bitcointalk Userscript Translator
// @namespace https://bitcointalk.org/
// @version 3.0.0
// @description Bitcointalk Translator with modern UI and multi-language support
// @author Crypto Community
// @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';
/* =========================================================
CONFIG
========================================================= */
const CONFIG = {
targetLanguage:
GM_getValue('btc_target_lang', 'en'),
sideBySide:
GM_getValue('btc_side_by_side', false),
autoTranslate:
GM_getValue('btc_auto_translate', false),
accent:
GM_getValue('btc_accent', '#f7931a')
};
/* =========================================================
STYLES
========================================================= */
GM_addStyle(`
.btc-translate-btn{
display:inline-flex;
align-items:center;
gap:5px;
padding:4px 10px;
margin-top:6px;
border-radius:8px;
background:rgba(247,147,26,.12);
border:1px solid rgba(247,147,26,.25);
color:${CONFIG.accent};
cursor:pointer;
font-size:12px;
font-weight:600;
transition:.2s ease;
}
.btc-translate-btn:hover{
transform:translateY(-1px);
background:rgba(247,147,26,.2);
}
.btc-translation-box{
margin-top:12px;
padding:14px;
border-radius:12px;
background:rgba(0,0,0,.18);
border-left:3px solid ${CONFIG.accent};
line-height:1.8;
animation:btcFade .2s ease;
word-break:break-word;
overflow-wrap:break-word;
}
.btc-head{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:10px;
}
.btc-tools{
display:flex;
gap:8px;
}
.btc-tool{
border:none;
padding:4px 8px;
border-radius:6px;
cursor:pointer;
background:rgba(255,255,255,.08);
color:#fff;
font-size:12px;
}
.btc-panel{
position:fixed;
right:20px;
bottom:20px;
width:260px;
z-index:999999;
background:#111;
color:#fff;
padding:16px;
border-radius:16px;
box-shadow:0 10px 30px rgba(0,0,0,.45);
font-family:Arial,sans-serif;
}
.btc-panel-title{
font-size:15px;
font-weight:bold;
color:${CONFIG.accent};
margin-bottom:14px;
}
.btc-panel select{
width:100%;
margin-bottom:12px;
padding:8px;
border-radius:8px;
border:none;
background:#222;
color:#fff;
font-size:14px;
}
.btc-thread-btn{
width:100%;
border:none;
padding:10px;
border-radius:10px;
background:${CONFIG.accent};
color:#fff;
cursor:pointer;
font-weight:bold;
}
.btc-arabic{
direction:rtl;
text-align:right;
font-family:
"Tahoma",
"Arial",
sans-serif;
line-height:2;
}
@keyframes btcFade{
from{
opacity:0;
transform:translateY(8px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@media(max-width:768px){
.btc-panel{
width:calc(100vw - 30px);
left:15px;
right:15px;
bottom:15px;
}
}
`);
/* =========================================================
CACHE
========================================================= */
class TranslationCache {
constructor(){
this.prefix = 'btc_translation_';
}
get(key){
return localStorage.getItem(
this.prefix + key
);
}
set(key, value){
localStorage.setItem(
this.prefix + key,
value
);
}
hash(str){
let hash = 0;
for(let i = 0; i < str.length; i++){
hash = ((hash << 5) - hash)
+ str.charCodeAt(i);
hash |= 0;
}
return hash;
}
}
const cache = new TranslationCache();
/* =========================================================
ESCAPE HTML
========================================================= */
function escapeHTML(str){
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
/* =========================================================
TRANSLATION ENGINE
========================================================= */
async function translateText(text, target = 'en') {
if(!text || text.length < 2){
return text;
}
const cacheKey =
cache.hash(text + target);
const cached =
cache.get(cacheKey);
if(cached){
return cached;
}
try{
const url =
`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
const response =
await fetch(url);
const data =
await response.json();
let translated = '';
data[0].forEach(item => {
translated += item[0];
});
if(!translated){
translated = text;
}
cache.set(cacheKey, translated);
return translated;
}catch(error){
console.error(
'Translation Error:',
error
);
return text;
}
}
/* =========================================================
TRANSLATION BOX
========================================================= */
function createTranslationBox(original, translated){
const isArabic =
CONFIG.targetLanguage === 'ar';
const box =
document.createElement('div');
box.className =
'btc-translation-box';
if(isArabic){
box.classList.add('btc-arabic');
}
box.innerHTML = `
<div class="btc-head">
<strong>
🌐 Translation
</strong>
<div class="btc-tools">
<button class="btc-tool btc-copy">
Copy
</button>
<button class="btc-tool btc-speak">
🔊
</button>
<button class="btc-tool btc-close">
✖
</button>
</div>
</div>
<div class="btc-content">
${
CONFIG.sideBySide
?
`
<div style="margin-bottom:14px;">
<b>Original:</b>
<div style="margin-top:6px;">
${escapeHTML(original)}
</div>
</div>
<div>
<b>Translated:</b>
<div style="margin-top:6px;">
${escapeHTML(translated)}
</div>
</div>
`
:
escapeHTML(translated)
}
</div>
`;
/* COPY */
box.querySelector('.btc-copy')
.addEventListener('click', () => {
navigator.clipboard.writeText(
translated
);
});
/* SPEECH */
box.querySelector('.btc-speak')
.addEventListener('click', () => {
const speech =
new SpeechSynthesisUtterance(
translated
);
speech.lang =
CONFIG.targetLanguage;
speechSynthesis.speak(speech);
});
/* CLOSE */
box.querySelector('.btc-close')
.addEventListener('click', () => {
box.remove();
});
return box;
}
/* =========================================================
PROCESS POSTS
========================================================= */
async function processPost(post){
if(post.dataset.btcReady){
return;
}
post.dataset.btcReady = 'true';
const footer =
post.querySelector('.smalltext');
const content =
post.querySelector('.post');
if(!footer || !content){
return;
}
const button =
document.createElement('div');
button.className =
'btc-translate-btn';
button.innerHTML =
'🌐 Translate';
footer.appendChild(button);
button.addEventListener(
'click',
async () => {
const old =
post.querySelector(
'.btc-translation-box'
);
if(old){
old.remove();
return;
}
const text =
content.innerText.trim();
if(!text){
return;
}
button.innerHTML =
'⏳ Translating...';
const translated =
await translateText(
text,
CONFIG.targetLanguage
);
const box =
createTranslationBox(
text,
translated
);
content.appendChild(box);
button.innerHTML =
'✅ Done';
setTimeout(() => {
button.innerHTML =
'🌐 Translate';
}, 1500);
}
);
if(CONFIG.autoTranslate){
button.click();
}
}
/* =========================================================
OBSERVER
========================================================= */
function observeForum(){
const observer =
new MutationObserver(() => {
const posts =
document.querySelectorAll(
'td.windowbg, td.windowbg2'
);
posts.forEach(
processPost
);
});
observer.observe(
document.body,
{
childList:true,
subtree:true
}
);
const initialPosts =
document.querySelectorAll(
'td.windowbg, td.windowbg2'
);
initialPosts.forEach(
processPost
);
}
/* =========================================================
SETTINGS PANEL
========================================================= */
function createPanel(){
const panel =
document.createElement('div');
panel.className =
'btc-panel';
panel.innerHTML = `
<div class="btc-panel-title">
Bitcointalk Translator
</div>
<label>
Translation Language
</label>
<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">India</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="nl">Nederlands</option>
<option value="yo">Nigeria</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">Skandinavisk</option>
<option value="tr">Turkish</option>
<option value="ur">Pakistan</option>
<option value="bn">বাংলা</option>
</select>
<label style="display:block;margin-bottom:10px;">
<input
type="checkbox"
id="btc-side"
${CONFIG.sideBySide ? 'checked' : ''}
>
Side-by-Side Mode
</label>
<label style="display:block;margin-bottom:14px;">
<input
type="checkbox"
id="btc-auto"
${CONFIG.autoTranslate ? 'checked' : ''}
>
Auto Translate
</label>
<button class="btc-thread-btn">
Translate Entire Thread
</button>
`;
document.body.appendChild(panel);
const lang =
panel.querySelector('#btc-lang');
lang.value =
CONFIG.targetLanguage;
lang.addEventListener(
'change',
e => {
CONFIG.targetLanguage =
e.target.value;
GM_setValue(
'btc_target_lang',
e.target.value
);
}
);
panel.querySelector('#btc-side')
.addEventListener(
'change',
e => {
CONFIG.sideBySide =
e.target.checked;
GM_setValue(
'btc_side_by_side',
e.target.checked
);
}
);
panel.querySelector('#btc-auto')
.addEventListener(
'change',
e => {
CONFIG.autoTranslate =
e.target.checked;
GM_setValue(
'btc_auto_translate',
e.target.checked
);
}
);
panel.querySelector('.btc-thread-btn')
.addEventListener(
'click',
async () => {
const buttons =
document.querySelectorAll(
'.btc-translate-btn'
);
for(const btn of buttons){
btn.click();
await sleep(400);
}
}
);
}
/* =========================================================
UTILITIES
========================================================= */
function sleep(ms){
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
/* =========================================================
SHORTCUTS
========================================================= */
function shortcuts(){
document.addEventListener(
'keydown',
e => {
if(
e.altKey
&&
e.key === 't'
){
const first =
document.querySelector(
'.btc-translate-btn'
);
if(first){
first.click();
}
}
}
);
}
/* =========================================================
INIT
========================================================= */
function init(){
console.log(
'Bitcointalk Translator Ultimate Loaded'
);
observeForum();
createPanel();
shortcuts();
}
init();
})();