Add a button on the BGA translation page that allows all the text to be translated on the page to be placed in a text block for easy copying.
// ==UserScript==
// @name BGA Translation TextBox Export
// @version 1.1.0
// @namespace ani20168
// @description Add a button on the BGA translation page that allows all the text to be translated on the page to be placed in a text block for easy copying.
// @author ani20168
// @license MIT
// @match https://boardgamearena.com/translation*
// @icon https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://boardgamearena.com&size=64
// @grant none
// ==/UserScript==
(function () {
'use strict';
/*** 常數 ***/
const SEPARATOR = '----------------------------------'; // 分隔線
/*** 主程式 ***/
function setup() {
// 避免重複建立
if (document.getElementById('textBoxExportButton')) { return; }
/* ---------- 1. Export 按鈕 ---------- */
const exportButton = document.createElement('input');
exportButton.type = 'button';
exportButton.value = 'TextBox Export';
exportButton.id = 'textBoxExportButton';
exportButton.className = 'bgabutton bgabutton_blue';
exportButton.style.marginLeft = '4px';
const exportArea = document.createElement('textarea');
exportArea.style.cssText = 'width:100%;height:200px;display:none;';
exportArea.readOnly = true;
exportButton.onclick = () => {
if (exportArea.style.display === 'block') {
exportArea.style.display = 'none';
} else {
const raws = Array.from(document.getElementsByClassName('readonly_textarea'))
.map(t => t.value.trim());
exportArea.value = raws.join('\n' + SEPARATOR + '\n');
exportArea.style.display = 'block';
}
};
/* ---------- 2. Import 按鈕 ---------- */
const importButton = document.createElement('input');
importButton.type = 'button';
importButton.value = 'TextBox Import';
importButton.id = 'textBoxImportButton';
importButton.className = 'bgabutton bgabutton_blue';
importButton.style.marginLeft = '4px';
const importArea = document.createElement('textarea');
importArea.style.cssText = 'width:100%;height:200px;display:none;';
importButton.onclick = () => {
if (importArea.style.display === 'block') {
// 第二次點擊 → 解析並分配
distributeTranslations(importArea.value);
importArea.style.display = 'none';
} else {
importArea.style.display = 'block';
}
};
/* ---------- 3. 把按鈕插入原本 UI ---------- */
const resetFilterButton = document.querySelector('#findreset_form > span');
resetFilterButton.appendChild(exportButton);
resetFilterButton.appendChild(importButton);
const btnContainer = document.getElementById('filters_buttons');
btnContainer.appendChild(exportArea);
btnContainer.appendChild(importArea);
}
/***
* 把整段翻譯文字依 SEPARATOR 切開並分配到每個 translation_block
* @param {string} rawText - 使用者貼上的整份翻譯結果
*/
function distributeTranslations(rawText) {
const pieces = rawText
.split(new RegExp(`\\s*${SEPARATOR}\\s*`, 'g'))
.map(t => t.trim())
.filter(Boolean);
const blocks = Array.from(document.getElementsByClassName('translation_block'));
let idx = 0;
blocks.forEach(block => {
const readonly = block.querySelector('.readonly_textarea');
const translated = block.querySelector('textarea[id^="translated_"]');
if (!readonly || !translated) { return; } // 保險檢查
// 如果還有對應翻譯就建立 UI
const piece = pieces[idx++];
if (piece !== undefined) {
addLoadUI(block, translated, piece);
}
});
}
/**
* 在指定翻譯區塊右側新增「載入文字框」+「確認載入」按鈕
* @param {HTMLElement} block - .translation_block
* @param {HTMLTextAreaElement} translatedArea - 原本用來輸入翻譯的 textarea
* @param {string} piece - 對應的翻譯文字
*/
function addLoadUI(block, translatedArea, piece) {
// 若已經加過就僅更新內容
let loadDiv = block.querySelector('.loaded_translation_zone');
if (!loadDiv) {
loadDiv = document.createElement('div');
loadDiv.className = 'loaded_translation_zone translation_meta';
// textarea
const loadTA = document.createElement('textarea');
loadTA.className = 'loaded_textarea';
loadTA.style.cssText = 'width:120%;height:140px;margin-top:4px;box-sizing:border-box;';
loadDiv.appendChild(loadTA);
// button
const btnWrap = document.createElement('div');
btnWrap.style.display = 'inline-block';
const confirmBtn = document.createElement('a');
confirmBtn.href = 'javascript:false;';
confirmBtn.tabIndex = -1;
confirmBtn.className = 'bgabutton bgabutton_blue';
confirmBtn.textContent = '確認載入';
btnWrap.appendChild(confirmBtn);
loadDiv.appendChild(btnWrap);
// 行為:點下"確認載入"
confirmBtn.onclick = () => {
// 1) 把翻譯文字塞進官方 textarea
translatedArea.value = loadTA.value.trim();
// 2) 觸發 input 事件,讓網站知道值變了
translatedArea.dispatchEvent(new Event('input', { bubbles: true }));
// 3) 模擬「點到 → 離開」以觸發自動上傳
translatedArea.focus();
// 用 setTimeout 保證事件順序:先 focus,再 blur
setTimeout(() => translatedArea.blur(), 0);
// 4) UI 標示
confirmBtn.textContent = '已載入';
confirmBtn.classList.add('disabled');
};
// 插入到右側 col-md-6 的最下方
const rightCol = block.querySelector('.col-md-6');
rightCol.appendChild(loadDiv);
}
// 更新文字
loadDiv.querySelector('.loaded_textarea').value = piece;
}
/* ---------- 啟動監聽 ---------- */
new MutationObserver(setup).observe(document, { childList: true, subtree: true });
setup();
})();