// ==UserScript==
// @name Simplified Chinese for light.gg
// @name:zh light.gg 简体中文支持
// @name:zh-CN light.gg 简体中文支持
// @namespace https://github.com/HZDeluxe
// @version 0.3.1
// @description Add Simplified Chinese weapons and perks display for light.gg
// @description:zh 为light.gg添加简体中文武器和特性支持
// @description:zh-CN 为light.gg添加简体中文武器和特性支持
// @author HZDeluxe
// @match https://www.light.gg/*
// ==/UserScript==
(function() {
'use strict';
var page = null;
var getItemDataOnce = wrapOnce(getItemData);
var modifyPerksRequestOnce = wrapOnce(modifyPerksRequest);
var lgg = {
data: {
item: { ready: false }
},
common: {
navbar: {
selector: '#top-nav',
ontranslate: translStatic
},
signup: {
selector: '#sign-up-nudge',
ontranslate: translStatic
}
},
pages: {
index: {
path: /^\/$/i,
elms: {}
},
item: {
path: /\/items\/(\d+)\//i,
elms: {
itemHeader: {
selector: '#main-column > .item-header',
ontranslate: translItemHeader
},
itemKeyPerks: {
selector: '#special-perks',
pretranslate: getItemDataOnce,
ontranslate: translKeyPerks
},
itemStats: {
selector: '#stat-container',
ontranslate: translStatic
},
itemPerks: {
selector: '#socket-container > .perks',
pretranslate: modifyPerksRequestOnce,
ontranslate: translItemPerks
},
itemYourRolls: {
selector: '#my-rolls',
dynamic: true,
pretranslate: modifyPerksRequestOnce,
ontranslate: function() {
var myRolls = document.querySelector('#my-rolls');
if(myRolls) {
// get vue instance and modify UpdateComputedRolls function
var vm2 = myRolls.__vue__;
var realUpdateRolls = vm2.UpdateComputedRolls;
vm2.UpdateComputedRolls = function() {
realUpdateRolls();
if(localStorage.getItem('lang') === 'chs') {
vm2.$nextTick(function(){
lgg.utils.addChsSuffix(vm2.$el);
});
} else {
vm2.$nextTick(function(){
lgg.utils.removeChsSuffix(vm2.$el);
});
}
};
}
},
ontoggle: function(chs) {
var title = document.querySelector('#my-rolls > h4');
if(title) {
title.childNodes[0].textContent = chs ? '你的 Roll' : 'Your Rolls';
}
var descs = document.querySelectorAll('#my-rolls > div:nth-of-type(1) a');
if(descs) {
descs[0].innerText = chs ? '评分依据' : 'Explain Grades';
descs[1].innerText = chs ? '推荐依据' : 'Explain Recommendations';
}
var refresh = document.querySelector('#my-rolls > button:nth-of-type(1)');
if(refresh) {
refresh.childNodes[1].textContent = chs ? '刷新' : 'Refresh';
}
document.body.dispatchEvent(new Event("doneRefreshing"));
}
},
itemYourRollsPlug: {
selector: '#my-rolls-plug',
ontranslate: translStatic
},
itemRelatedCollectible: {
selector: '#related-collectible',
pretranslate: getItemDataOnce,
ontranslate: translRelatedCollectible
},
itemLoreTitle: {
selector: '#main-column > h3',
ontranslate: function(elm) {
if(elm.translated) {
elm.translated.innerText = '传奇故事';
}
elm.translDone = true;
}
},
itemLoreContent: {
selector: '#lore-content',
ontranslate: translLoreContent
},
itemSidebar: {
selector: '#sidebar-container',
ontranslate: translItemSidebar
},
itemTabbedContent: {
selector: '#tabbed-content-container',
ontranslate: translTabbedContent
},
}
}
},
utils: {
getCurrentPage: function() {
var p = { elms: {} };
for (var propPage in lgg.pages) {
var path = lgg.pages[propPage].path;
if(path.test(location.pathname)) {
p = lgg.pages[propPage];
break;
}
}
for (var propCommon in lgg.common) {
p.elms[propCommon] = lgg.common[propCommon];
}
return p;
},
getItemId: function() {
var matches = location.pathname.match(/\/items\/(\d+)\//i);
return matches[1];
},
isTranslDone: function() {
for (var prop in page.elms) {
var elm = page.elms[prop];
if(!elm.dynamic && !elm.translDone) {
return false;
}
}
return true;
},
addChsSuffix: function(elm) {
var itemsWithDataId = elm.querySelectorAll('.show-hover[data-id]');
itemsWithDataId.forEach(function(item) {
item.setAttribute('data-id', item.dataset.id.replace('-chs', '') + '-chs');
});
},
removeChsSuffix: function(elm) {
var itemsWithDataId = elm.querySelectorAll('.show-hover[data-id]');
itemsWithDataId.forEach(function(item) {
item.setAttribute('data-id', item.dataset.id.replace('-chs', ''));
});
},
translateTree: function(elm) {
if(!elm) {
return;
}
var treeWalker = document.createTreeWalker(
elm,
NodeFilter.SHOW_TEXT
);
var currentNode = treeWalker.currentNode;
while(currentNode) {
var text = currentNode.textContent.trim();
var translatedText = dict[text];
if(translatedText) {
currentNode.textContent = translatedText;
}
currentNode = treeWalker.nextNode();
}
}
}
};
var dict = {
"Splicer": "永夜",
"Chosen": "天选",
"Beyond Light": "凌光之刻",
"Trials": "试炼",
"Raids": "突袭",
"Lore": "传奇故事",
"Exotic Gear": "异域装备",
"Gambit": "智谋",
"Vanguard": "先锋",
"Crucible": "熔炉",
"The Crucible": "熔炉",
"Iron Banner": "铁旗",
"Emblems": "徽标",
"Checklists": "清单",
"Eververse": "永恒之诗",
"Season 14": "14赛季",
"Override Gear": "超控装备",
"Vault of Glass Gear": "玻璃宝库装备",
"New Exotics": "新增异域装备",
"New Armor": "新增护甲",
"New Weapons": "新增武器",
"New Cosmetics": "新增装饰",
"New Quests": "新增任务",
"Trials Gear": "试炼装备",
"All Seasons": "所有赛季",
"Exotics": "异域装备",
"Weapons": "武器",
"All Armor": "所有护甲",
"Titan Gear": "泰坦装备",
"Hunter Gear": "猎人装备",
"Warlock Gear": "术士装备",
"Cosmetics": "装饰",
"Inventory Items": "物品栏",
"Quests": "任务",
"Bounties": "悬赏",
"Legend": "传说",
"Collections": "收藏品",
"Triumphs": "成就",
"Badges": "证章",
"Seals": "印章",
"Vendors": "商人",
"God Roll Hub": "God Roll 中心",
"God Roll Finder": "God Roll 查找",
"Roll Appraiser": "Roll 评估",
"Tooltip Builder": "Tooltip 构建",
"Season Pass Tracker": "季票进度追踪",
"The Director": "导航器",
"API Update Tracker": "API 更新追踪",
"Item Comparer": "装备对比",
// navbar
"Home": "首页",
"Sign In": "登录",
"Database": "数据库",
"God Rolls": "God Rolls",
"Tools": "工具",
"Collection": "收藏",
"Leaderboard": "排行榜",
"Light Mode": "浅色模式",
"Dark Mode": "深色模式",
"Sign Out": "退出账号",
// sign up banner
"Welcome to light.gg!": "欢迎来到 light.gg!",
"Sign in with your Bungie account to track your collection, review your favorite gear, compete on the leaderboards, and more!":
"登录 Bungie 账号来跟踪你的收藏、评价你最爱的装备,又或者是在排行榜上一决高下,更多功能等你发现!",
"Sign in with your Bungie account to unlock all light.gg features!":
"登录 Bungie 账号以解锁 light.gg 所有功能!",
"Let's Go": "马上登录",
"Not Now": "不是现在",
// item page
"Weapon Stats": "武器属性",
"Hidden Stats": "隐藏属性",
"Perks": "特性",
"Curated Roll": "官 Roll",
"Not all curated rolls actually drop in-game.": "不是所有的官 Roll都能在游戏里掉落。",
"Learn more": "了解更多",
"Show Recommendations": "显示推荐",
"Hide Recommendations": "隐藏推荐",
"Random Rolls": "随机 Roll",
"Item is eligible for random rolls.": "该道具掉落时可以获得随机特性。",
"Item has recommended perks from the community.": "该道具具有社区推荐的特性。",
"Community Average Roll": "社区最常用 Roll",
"Related Collectible": "相关收集品",
"Contained In": "产出于",
"Reward From": "奖励于",
"Related Vendors": "相关商人",
"Name": "名称",
"Class": "职业",
"Titan": "泰坦",
"Hunter": "猎人",
"Warlock": "术士",
"Rarity": "稀有度",
"Common": "普通",
"Uncommon": "罕见",
"Rare": "稀有",
"Legendary": "传说",
"Exotic": "异域",
"Slot": "槽位",
"Kinetic": "动能",
"Energy": "能量",
"Power": "威能",
"Type": "类别",
"Triumph": "成就",
"Share": "分享",
"Compare": "对比",
"View 3D": "查看 3D",
"View in 3D": "查看 3D模型",
"Screenshots": "屏幕截图",
"Details": "详情",
"Deals": "产生 ",
"KINETIC": "动能",
"SOLAR": "烈日",
"VOID": "虚空",
"ARC": "电弧",
"STASIS": "冰影",
"damage": " 伤害",
"Uses": "使用 ",
"PRIMARY": "主武器",
"SPECIAL": "特殊武器",
"HEAVY": "重武器",
"ammo": " 弹药",
"Introduced in": "加入于",
"Kinetic Weapon": "动能武器",
"Energy Weapon": "能量武器",
"Power Weapon": "威能武器",
"Instance Item": "可实例化道具",
"Equippable": "可装备",
"Community Rarity": "社区稀有度",
"Version History": "版本历史",
"MODIFIED": "修改于",
"ADDED": "添加于",
"Tags": "标签",
"Other Languages": "其他语言",
// weapon stats
"Swing Speed": "挥舞速度",
"Efficiency": "防御效率",
"Defense": "防御抗性",
"Charge Rate": "充能效率",
"Charge Time": "充能时间",
"Ammo Capacity": "弹药容量",
"Blast Radius": "爆炸范围",
"Velocity": "弹头速度",
"Impact": "伤害",
"Range": "射程",
"Accuracy": "精度",
"Stability": "稳定性",
"Handling": "操控性",
"Reload Speed": "填装速度",
"Rounds Per Minute": "每分钟发射数",
"Magazine": "弹匣",
"Draw Time": "蓄力时间",
"Aim Assistance": "辅助瞄准",
"Inventory Size": "物品栏空间",
"Zoom": "变焦",
"Recoil": "后坐力",
"Bounce Intensity": "回弹强度",
"Bounce Direction": "回弹方向",
"Tends Vertical": "垂直",
"Tends Left": "偏左",
"Tends Right": "偏右",
// sign up plug
"Sign in to see the rolls you own for this weapon":
"登录以查看该武器你拥有的所有 Roll",
"and compare them against community suggested rolls!":
"并且获取这些 Roll 的社区常用度评分!"
};
init();
function init() {
// load external script
loadMorphdom();
// init language setting
if (!localStorage.getItem('lang')) {
localStorage.setItem('lang', 'chs');
}
// fix weird "文言", Traditional Chinese should be "繁体中文"
var chtModalLink = document.querySelector('#localemodal .modal-body a[href^="/db/zh-cht/"]');
if(chtModalLink) {
chtModalLink.lastChild.textContent = ' 繁体中文';
}
var chtSidebarImg = document.querySelector('#sidebar-locales img[title="文言"]');
if(chtSidebarImg) {
chtSidebarImg.title = '繁体中文';
}
// set current page
page = lgg.utils.getCurrentPage();
if(!page) {
return;
}
translateElms();
createToggleButton();
createSearchButton();
createSearchBar();
toggleTranslation(localStorage.getItem('lang') === 'chs');
}
/* function defines */
// load morphdom and attach it to body
function loadMorphdom() {
var s = document.createElement('script');
s.setAttribute('src', 'https://cdn.jsdelivr.net/npm/morphdom@2.6.1/dist/morphdom-umd.min.js');
s.addEventListener('load', function() {
var s = document.createElement('script');
s.text = 'document.body.__morphdom__ = window.morphdom;';
document.head.appendChild(s);
});
document.head.appendChild(s);
}
// save and translate DOM elements in memory
function translateElms() {
function translateSingleElm(elm) {
var sel = elm.selector;
if(sel) {
if(elm.pretranslate) {
elm.pretranslate();
}
if(!elm.dynamic) {
var domElm = document.querySelector(sel);
if(domElm) {
elm.original = domElm.cloneNode(true);
elm.translated = domElm.cloneNode(true);
}
}
if(elm.ontranslate) {
elm.ontranslate(elm);
}
}
}
for (var prop in page.elms) {
translateSingleElm(page.elms[prop]);
}
}
// create a toggle button
function createToggleButton() {
var btnChs = document.createElement('button');
btnChs.classList.add('btn', 'btn-orange');
btnChs.style.fontSize = '20px';
btnChs.style.position = 'fixed';
btnChs.style.right = '20px';
btnChs.style.top = '20px';
btnChs.style.zIndex = '5000';
function toggleButton(lang) {
if(lang === 'chs') {
btnChs.innerText = '简';
btnChs.style.backgroundColor = '#f70';
} else {
btnChs.innerText = '原';
btnChs.style.backgroundColor = 'gray';
}
}
btnChs.onclick = function() {
var lang = localStorage.getItem('lang');
lang = lang === 'chs' ? 'others' : 'chs';
localStorage.setItem('lang', lang);
toggleButton(lang);
toggleTranslation(lang === 'chs');
};
toggleButton(localStorage.getItem('lang'));
document.body.append(btnChs);
}
// create search button
function createSearchButton() {
var onStatus = true;
var btnSearch = document.createElement('button');
btnSearch.classList.add('btn', 'btn-orange');
btnSearch.style.fontSize = '20px';
btnSearch.style.position = 'fixed';
btnSearch.style.right = '72px';
btnSearch.style.top = '20px';
btnSearch.style.zIndex = '5000';
btnSearch.style.backgroundColor = 'gray';
btnSearch.innerText = '搜';
function toggleButton() {
if(onStatus) {
btnSearch.innerText = '搜';
} else {
btnSearch.innerText = '关';
}
}
btnSearch.onclick = function() {
var searchBar = document.querySelector('#sc-search-bar');
if(onStatus) {
searchBar.style.display = 'block';
document.querySelector('#sc-search-input').focus();
} else {
searchBar.style.display = 'none';
}
onStatus = !onStatus;
toggleButton();
}
document.body.append(btnSearch);
}
// create search bar
function createSearchBar() {
var searchBarCss = `
<style type="text/css">
#sc-search-bar {
display: none;
width: 300px;
position: fixed;
top: 70px;
right: 20px;
z-index: 5000;
}
@media (min-width: 500px) {
#sc-search-bar {
top: 20px;
right: 124px;
}
}
#sc-search-input {
height: 40px;
width: 100%;
max-width: 300px;
border: 2px solid gray;
font-size: 24px;
background-color: #1a1a20;
color: #aaa;
}
#sc-search-result {
border: none;
height: 500px;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', searchBarCss);
var searchBarHtml = `
<div id="sc-search-bar">
<input id="sc-search-input" type="text">
<iframe id="sc-search-result" src="about:blank"></iframe>
</div>`;
document.body.insertAdjacentHTML('beforeend', searchBarHtml);
var searchInput = document.querySelector('#sc-search-input');
var searchResult = document.querySelector('#sc-search-result');
var timeout = null;
searchInput.oninput = function() {
if(timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function() {
searchResult.src = "https://sc4lightgg-worker.haozi.workers.dev/search?q=" + encodeURIComponent(searchInput.value);
}, 1000);
}
}
// switch language by replacing html
function toggleTranslation(chs) {
// wait until morphdom loaded and translation done
if(!document.body.__morphdom__ ||
(chs && !lgg.utils.isTranslDone()) ) {
setTimeout(function() {
toggleTranslation(chs);
}, 200);
return;
}
var morphdom = document.body.__morphdom__;
function toggleSingleElm(elm) {
var sel = elm.selector;
if(sel) {
if(!elm.dynamic) {
var domElm = document.querySelector(sel);
if(domElm) {
morphdom(domElm, chs ? elm.translated : elm.original);
}
}
if(elm.ontoggle) {
elm.ontoggle(chs);
}
}
}
for (var prop in page.elms) {
toggleSingleElm(page.elms[prop]);
}
}
// produce function that can only be called once
function wrapOnce(fn) {
var done = false;
return function () {
if (!done) {
done = true;
fn();
}
}
}
// request Simplified Chinese weapon data
// and use it to replace the original text
function getItemData() {
var item = lgg.data.item;
item.id = lgg.utils.getItemId();
var xhr = new XMLHttpRequest();
xhr.open('GET', '/db/items/hover/' + item.id + '?lang=zh-chs');
xhr.send();
xhr.onload = function() {
if (xhr.responseText) {
var parser = new DOMParser();
var doc = parser.parseFromString(xhr.responseText, "text/html");
item.name = doc.querySelector('.hover-item-header h2').innerText.trim();
item.source = doc.querySelector('.collectible-hint .inner-description-container').lastChild.textContent;
item.intrinsicPerk = doc.querySelector('.hover-item-intrinsic-perk h4').innerHTML;
item.intrinsicPerkDesc = doc.querySelector('.hover-item-intrinsic-perk h4 + div').innerHTML;
item.traitPerk = doc.querySelector('.hover-item-trait-perk h4').innerText;
item.traitPerkDesc = doc.querySelector('.hover-item-trait-perk h4 + div').innerText;
item.ready = true;
}
};
}
// modified the perks request to get Simplified Chinese data
function modifyPerksRequest() {
function doModifyPerksRequest() {
var realOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function() {
var url = arguments['1'];
if (url.startsWith('/db/items/hover/')
&& url.includes('-chs?lang=')) {
url = url.replace('-chs', ''); // remove added -chs suffix
var splits = url.split('=');
url = splits[0] + '=zh-chs';
}
arguments['1'] = url;
return realOpen.apply(this, arguments);
};
}
var script = document.createElement("script");
script.textContent = "(" + doModifyPerksRequest.toString() + ")();";
document.body.appendChild(script);
}
/* actual translation functions */
// translate static UI elements using dict
function translStatic(elm) {
lgg.utils.translateTree(elm.translated);
elm.translDone = true;
}
// translate item header
function translItemHeader(ih) {
lgg.utils.translateTree(ih.translated);
var slots = {
"1498876634": "动能武器",
"2465295065": "能量武器",
"953998645": "威能武器"
};
var classes = ["泰坦", "猎人", "术士", ""];
var item = {};
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.bungie.net/Platform/Destiny2/Manifest/DestinyInventoryItemDefinition/' + lgg.utils.getItemId() + '/?lc=zh-chs');
xhr.setRequestHeader('X-API-Key', 'd4ea1e95b5394ffdb46908af0275f324');
xhr.setRequestHeader('content-type', 'application/json');
xhr.send();
xhr.onload = function() {
if (xhr.responseText) {
var response = JSON.parse(xhr.responseText).Response;
item.name = response.displayProperties.name;
item.slot = slots[response.equippingBlock.equipmentSlotTypeHash];
item.tier = response.inventory.tierTypeName;
item.type = response.itemTypeDisplayName;
item.class = classes[response.classType];
item.flavor = response.flavorText;
item.source = response.displaySource;
ih.translated.querySelector('.item-name h2').childNodes[0].textContent = item.name;
ih.translated.querySelector('.weapon-type').innerText =
item.tier + ' / ' + (item.class ? item.class + ' / ' : '') +
item.slot + ' / ' + item.type;
var flavors = ih.translated.querySelectorAll('.flavor-text > h4');
flavors[0].innerText = item.flavor;
if(flavors[1]) {
flavors[1].innerText = item.source;
}
ih.translDone = true;
}
};
}
// translate key perks
function translKeyPerks(kp) {
var item = lgg.data.item;
if(!item.ready) {
setTimeout(function () {
translKeyPerks(kp);
}, 200);
return;
}
var keyPerks = kp.translated.querySelectorAll('.key-perk');
keyPerks[0].querySelector('h4').innerHTML = item.intrinsicPerk;
keyPerks[0].querySelector('h4 + div').innerHTML = item.intrinsicPerkDesc;
keyPerks[1].querySelector('h4').innerHTML = item.traitPerk;
keyPerks[1].querySelector('h4 + div').innerHTML = item.traitPerkDesc;
kp.translDone = true;
}
// translate item perks
function translItemPerks(ip) {
lgg.utils.addChsSuffix(ip.translated);
lgg.utils.translateTree(ip.translated);
// translate community average roll description
var descLink = ip.translated.querySelector('a[data-target="#community-roll-modal"]');
if(descLink) {
var desc = descLink.parentElement.childNodes[0];
var matches = desc.textContent.match(/on (.+) copies/i);
desc.textContent = '下面是基于 ' + matches[1] + ' 份该武器统计出的最常装备的特性。';
}
ip.translDone = true;
}
// translate related collectible
function translRelatedCollectible(rc) {
var item = lgg.data.item;
if(!item.ready) {
setTimeout(function () {
translRelatedCollectible(rc);
}, 200);
return;
}
rc.translated.querySelector('.item-header .item-name h2').childNodes[0].textContent = item.name;
rc.translated.querySelector('.source-line').innerText = item.source;
lgg.utils.translateTree(rc.translated);
rc.translDone = true;
}
// translate lore content
function translLoreContent(lc) {
var lore = {};
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.bungie.net/Platform/Destiny2/Manifest/DestinyLoreDefinition/' + lgg.utils.getItemId() + '/?lc=zh-chs');
xhr.setRequestHeader('X-API-Key', 'd4ea1e95b5394ffdb46908af0275f324');
xhr.setRequestHeader('content-type', 'application/json');
xhr.send();
xhr.onload = function() {
if (xhr.responseText) {
var response = JSON.parse(xhr.responseText).Response;
if (response) {
lore.name = response.displayProperties.name;
lore.subtitle = response.subtitle;
lore.description = response.displayProperties.description;
lc.translated.querySelector('h2 > i').nextSibling.textContent = lore.name;
lc.translated.querySelector('p > em').innerText = lore.subtitle;
lc.translated.querySelector('p.lore-desc').innerText = lore.description;
}
lc.translDone = true;
}
}
}
// translate item sidebar
function translItemSidebar(is) {
lgg.utils.translateTree(is.translated);
// translate community rarity description
var rarityDesc = is.translated.querySelector('#community-rarity > div > span');
var percent = rarityDesc.childNodes[1].innerText;
rarityDesc.innerHTML = '被 <strong>' + percent + '</strong> 的light.gg <br> 守护者发现过';
is.translDone = true;
}
function translTabbedContent(tc) {
lgg.utils.addChsSuffix(tc.translated);
lgg.utils.translateTree(tc.translated);
// translate reviews (num)
var reviewTab = tc.translated.querySelector('#review-tab');
if(reviewTab) {
var matches = reviewTab.innerText.match(/\(\d+\)/i);
reviewTab.innerText = '评价 ' + matches[0];
}
tc.translDone = true;
}
})();