// ==UserScript==
// @name Scryfall卡牌汉化
// @description 为Scryfall没有中文的卡牌添加汉化,所有汉化数据均来自中文卡查sbwsz.com
// @author lieyanqzu
// @license GPL
// @namespace http://github.com/lieyanqzu
// @icon https://scryfall.com/favicon.ico
// @version 1.0
// @match *://scryfall.com/card/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
const API_BASE_URL = 'https://api.sbwsz.com/card';
const TYPE_NAME_TRANSLATIONS_URL = 'https://sbwsz.com/static/typeName.json';
let typeNameTranslations = null;
let isChineseDisplayed = GM_getValue('defaultToChinese', false);
GM_registerMenuCommand('默认显示中文: ' + (isChineseDisplayed ? '开' : '关'), toggleDefaultLanguage);
function toggleDefaultLanguage() {
const newDefault = !GM_getValue('defaultToChinese', false);
GM_setValue('defaultToChinese', newDefault);
location.reload();
}
async function getChineseCardData(setCode, collectorNumber) {
const apiUrl = `${API_BASE_URL}/${setCode}/${collectorNumber}`;
try {
const response = await makeRequest('GET', apiUrl);
const data = JSON.parse(response.responseText);
const scryfallFaceCount = document.querySelectorAll('.card-text-title').length || 1;
if (scryfallFaceCount === 1) {
return processSingleFacedCard(data.data[0]);
} else if (data.type === 'double' && data.data.length === 2) {
return processDoubleFacedCard(data.data);
} else if (data.type === 'normal' && data.data.length > 0) {
return processSingleFacedCard(data.data[0]);
}
throw new Error('无法获取中文卡牌数据');
} catch (error) {
console.error('获取中文卡牌数据失败:', error);
throw error;
}
}
function processCardFace(cardData) {
const name = cardData.zhs_faceName || cardData.translatedName || cardData.zhs_name || cardData.officialName || cardData.name;
return {
name,
text: processText(cardData.translatedText || cardData.zhs_text || cardData.officialText || cardData.text, name),
flavorText: processText(cardData.zhs_flavorText || cardData.translatedFlavorText || cardData.flavorText)
};
}
const processDoubleFacedCard = data => ({
front: processCardFace(data[0]),
back: processCardFace(data[1])
});
const processSingleFacedCard = cardData => processCardFace(cardData);
function processText(text, cardName) {
if (!text) return text;
text = text.replace(/\\n/g, '\n');
return cardName ? text.replace(/CARDNAME/g, cardName) : text;
}
async function getTypeNameTranslations() {
if (typeNameTranslations) return typeNameTranslations;
try {
const response = await makeRequest('GET', TYPE_NAME_TRANSLATIONS_URL);
typeNameTranslations = JSON.parse(response.responseText);
return typeNameTranslations;
} catch (error) {
console.error('获取类别翻译数据失败:', error);
throw error;
}
}
async function translateType(englishType) {
const translations = await getTypeNameTranslations();
return englishType.trim().split('—').map((part, index) => {
const words = part.trim().split(/\s+/);
const translatedWords = words.map(word => translations[word] || word);
return index === 0 ? translatedWords.join('') : translatedWords.join('/');
}).join(' ~ ');
}
async function main() {
const match = window.location.pathname.match(/\/card\/(\w+)\/([^/]+)\//);
if (!match) {
console.error('无法从 URL 获取 setCode 和 collectorNumber');
return;
}
const [, setCode, collectorNumber] = match;
try {
saveOriginalContent();
addToggleButton(true);
const chineseData = await getChineseCardData(setCode, collectorNumber);
const scryfallFaceCount = document.querySelectorAll('.card-text-title').length || 1;
if (scryfallFaceCount === 1 || !chineseData.front) {
await saveSingleFacedCard(chineseData);
} else {
await saveDoubleFacedCard(chineseData);
}
updateToggleButton();
if (isChineseDisplayed) {
isChineseDisplayed = false;
await toggleLanguage({ preventDefault: () => {}, target: document.querySelector('.print-langs-item') }, false);
}
} catch (error) {
console.error('获取或保存中文数据时出错:', error);
updateToggleButton(true);
}
}
function addToggleButton(loading = false) {
const printLangs = document.querySelector('.print-langs');
if (!printLangs) return;
const toggleLink = document.createElement('a');
toggleLink.className = 'print-langs-item';
toggleLink.href = 'javascript:void(0);';
toggleLink.textContent = loading ? '加载中...' : (isChineseDisplayed ? '原文' : '汉化');
toggleLink.style.cursor = loading ? 'wait' : 'pointer';
if (!loading) {
toggleLink.addEventListener('click', toggleLanguage);
}
printLangs.insertBefore(toggleLink, printLangs.firstChild);
}
function updateToggleButton(error = false) {
const toggleLink = document.querySelector('.print-langs-item');
if (toggleLink) {
toggleLink.textContent = error ? '加载失败' : (isChineseDisplayed ? '原文' : '汉化');
toggleLink.style.cursor = error ? 'not-allowed' : 'pointer';
toggleLink[error ? 'removeEventListener' : 'addEventListener']('click', toggleLanguage);
}
}
async function toggleLanguage(event, updateButton = true) {
event.preventDefault();
const elements = document.querySelectorAll('.card-text-card-name, .card-text-type-line, .card-text-oracle, .card-text-flavor');
const toggleLink = event.target;
if (elements.length === 0 || !elements[0].dataset.chineseContent) {
console.error('中文数据尚未加载完成');
return;
}
isChineseDisplayed = !isChineseDisplayed;
elements.forEach(el => {
if (el.dataset.chineseContent) {
[el.innerHTML, el.dataset.chineseContent] = [el.dataset.chineseContent, el.innerHTML];
}
});
if (updateButton) {
toggleLink.textContent = isChineseDisplayed ? '原文' : '汉化';
}
}
function saveOriginalContent() {
document.querySelectorAll('.card-text-card-name, .card-text-type-line, .card-text-oracle, .card-text-flavor').forEach(el => {
el.dataset.originalContent = el.innerHTML;
});
}
async function saveSingleFacedCard(chineseData) {
await saveCardFace(document, document, chineseData, 0);
console.log('中文数据已保存');
}
async function saveDoubleFacedCard(chineseData) {
const cardTextDiv = document.querySelector('.card-text');
if (!cardTextDiv) {
console.error('无法找到卡牌文本元素');
return;
}
const cardFaces = cardTextDiv.querySelectorAll('.card-text-title');
if (cardFaces.length !== 2) {
console.error('无法找到双面卡牌的元素');
return;
}
await Promise.all([
saveCardFace(cardTextDiv, cardFaces[0], chineseData.front, 0),
saveCardFace(cardTextDiv, cardFaces[1], chineseData.back, 1)
]);
}
async function saveCardFace(cardTextDiv, cardFace, faceData, faceIndex) {
await Promise.all([
saveElementText('.card-text-card-name', faceData.name, cardFace),
saveType(cardTextDiv.querySelectorAll('.card-text-type-line')[faceIndex], faceData.name),
saveCardText('.card-text-oracle', faceData.text, cardTextDiv, faceIndex),
faceData.flavorText ? saveCardText('.card-text-flavor', faceData.flavorText, cardTextDiv, faceIndex) : Promise.resolve()
]);
}
async function saveElementText(selector, text, parent = document) {
const element = parent.querySelector(selector);
if (element) {
element.dataset.chineseContent = text;
}
}
async function saveType(typeLineElement, cardName) {
if (!typeLineElement) return;
const colorIndicator = typeLineElement.querySelector('.color-indicator');
const typeText = typeLineElement.textContent.replace(colorIndicator ? colorIndicator.textContent.trim() : '', '').trim();
try {
const translatedType = await translateType(typeText);
typeLineElement.dataset.chineseContent = colorIndicator
? `${colorIndicator.outerHTML} ${translatedType}`
: translatedType;
} catch (error) {
console.error('翻译类型时出错:', error);
}
}
async function saveCardText(selector, text, parent = document, index = 0) {
const elements = parent.querySelectorAll(selector);
if (elements[index]) {
const preservedHtml = await preserveManaSymbols(elements[index].innerHTML, text);
elements[index].dataset.chineseContent = `<p>${preservedHtml.replace(/\n/g, '</p><p>')}</p>`;
}
}
async function preserveManaSymbols(originalHtml, chineseText) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = originalHtml;
const manaSymbols = tempDiv.querySelectorAll('abbr.card-symbol');
const symbolMap = new Map();
manaSymbols.forEach(symbol => {
const symbolText = symbol.title.match(/\{(.+?)\}/)?.[0] || symbol.textContent;
symbolMap.set(symbolText, (symbolMap.get(symbolText) || []).concat(symbol.outerHTML));
});
return Array.from(symbolMap).reduce((result, [symbolText, htmls]) => {
let index = 0;
return result.replace(new RegExp(escapeRegExp(symbolText), 'g'), () => {
const html = htmls[index];
index = (index + 1) % htmls.length;
return html;
});
}, chineseText);
}
const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
function makeRequest(method, url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method,
url,
onload: resolve,
onerror: reject
});
});
}
main();
})();