commit 435399bd7bc0881e17a3101422b3bbe81573300f Author: xkeyC <3334969096@qq.com> Date: Mon Oct 9 19:06:39 2023 +0800 Init diff --git a/background.js b/background.js new file mode 100644 index 0000000..c13c17f --- /dev/null +++ b/background.js @@ -0,0 +1,151 @@ +let dataVersion = null + +chrome.runtime.onInstalled.addListener(function () { + _checkVersion().then(_ => { }); + console.log("SWTT init"); +}); + +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + if (request.action === "_loadLocalizationData") { + _initLocalization(request.url).then(data => { + sendResponse({ result: data }); + }); + return true; + } +}); + +async function _checkVersion() { + dataVersion = await _getJsonData("versions.json"); + console.log("Localization Version ==="); + console.log(dataVersion); +} + +async function _initLocalization(url) { + console.log("url ===" + url); + if (dataVersion == null) { + await _checkVersion(); + return _initLocalization(url); + } + let v = dataVersion + // TODO check version + let data = {}; + data["zh-CN"] = await _getJsonData("zh-CN-rsi.json", { cacheKey: "zh-CN", version: v.rsi }); + + if (url.includes("robertsspaceindustries.com")) { + data["concierge"] = await _getJsonData("concierge.json", { cacheKey: "concierge", version: v.concierge }); + data["orgs"] = await _getJsonData("orgs.json", v.orgs); + data["address"] = await _getJsonData("addresses.json", { cacheKey: "orgs", version: v.addresses }); + data["hangar"] = await _getJsonData("hangar.json", { cacheKey: "hangar", version: v.hangar }); + } else { + data["UEX"] = await _getJsonData("zh-CN-uex.json", { cacheKey: "uex", version: v.uex }); + } + // update data + let replaceWords = getLocalizationResource(data, "zh-CN"); + + function addLocalizationResource(key) { + replaceWords.push(...getLocalizationResource(data, key)); + } + + if (url.includes("robertsspaceindustries.com")) { + const org = "https://robertsspaceindustries.com/orgs"; + const citizens = "https://robertsspaceindustries.com/citizens"; + const organization = "https://robertsspaceindustries.com/account/organization"; + const concierge = "https://robertsspaceindustries.com/account/concierge"; + const referral = "https://robertsspaceindustries.com/account/referral-program"; + const address = "https://robertsspaceindustries.com/account/addresses"; + const hangar = "https://robertsspaceindustries.com/account/pledges"; + if (url.startsWith(org) || url.startsWith(citizens) || url.startsWith(organization)) { + replaceWords.push({ "word": 'members', "replacement": '名成员' }); + addLocalizationResource("orgs"); + } + if (url.startsWith(address)) { + addLocalizationResource("address"); + } + + if (url.startsWith(referral)) { + replaceWords.push( + { "word": 'Total recruits: ', "replacement": '总邀请数:' }, + { "word": 'Prospects ', "replacement": '未完成的邀请' }, + { "word": 'Recruits', "replacement": '已完成的邀请' } + ); + } + + if (url.startsWith(concierge)) { + addLocalizationResource("concierge"); + } + + if (url.startsWith(hangar)) { + addLocalizationResource("hangar"); + } + } else { + addLocalizationResource("UEX"); + } + return replaceWords; +} + + +function getLocalizationResource(localizationResource, key) { + const localizations = []; + const dict = localizationResource[key]?.["dict"]; + if (typeof dict === "object") { + for (const [k, v] of Object.entries(dict)) { + const trimmedKey = k + .toString() + .trim() + .toLowerCase() + .replace(/\xa0/g, ' ') + .replace(/\s{2,}/g, ' '); + localizations.push({ "word": trimmedKey, "replacement": v.toString() }); + } + } + return localizations; +} + +async function _getJsonData(fileName, { cacheKey = "", version = null } = {}) { + url = "https://ch.citizenwiki.cn/json-files/" + fileName; + const box = await getLocalStorage(); + if (cacheKey && cacheKey !== "") { + const localVersion = await getLocalData(`${cacheKey}_version`); + const data = await getLocalData(cacheKey); + if (data && typeof data === 'object' && Object.keys(data).length > 0 && localVersion === version) { + return data; + } + } + const startTime = new Date(); + const response = await fetch(url, { method: 'GET', mode: 'cors' }); + const endTime = new Date(); + const data = await response.json(); + if (cacheKey && cacheKey !== "") { + console.log(`update ${cacheKey} v == ${version} time == ${(endTime - startTime) / 1000}s`); + await setLocalData(cacheKey, data); + await setLocalData(`${cacheKey}_version`, version); + } + return data; +} + + +function getLocalStorage() { + return new Promise((resolve) => { + chrome.storage.local; + resolve(); + }); +} + +function getLocalData(key) { + return new Promise((resolve) => { + chrome.storage.local.get([key], (result) => { + const data = result[key]; + resolve(data || null); + }); + }); +} + +function setLocalData(key, data) { + return new Promise((resolve) => { + const newData = {}; + newData[key] = data; + chrome.storage.local.set(newData, () => { + resolve(); + }); + }); +} diff --git a/core.js b/core.js new file mode 100644 index 0000000..da5896f --- /dev/null +++ b/core.js @@ -0,0 +1,158 @@ +let replaceLocalesMap = { "k": "v" }; + +function InitWebLocalization() { + // init script + let scriptTimeAgo = document.createElement('script'); + scriptTimeAgo.src = 'https://cdn.bootcdn.net/ajax/libs/timeago.js/4.0.2/timeago.full.min.js'; + document.head.appendChild(scriptTimeAgo); + if (typeof $ === 'undefined') { + console.log("loading JQ"); + let scriptJquery = document.createElement('script'); + scriptJquery.src = 'https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js'; + document.head.appendChild(scriptJquery); + } + LocalizationWatchUpdate(); + // load Data + _loadLocalizationData(); +} + +function LocalizationWatchUpdate() { + const m = window.MutationObserver || window.WebKitMutationObserver; + const observer = new m(function (mutations, observer) { + for (let mutationRecord of mutations) { + for (let node of mutationRecord.addedNodes) { + traverseElement(node); + } + } + }); + + observer.observe(document.body, { + subtree: true, + characterData: true, + childList: true, + }); + if (window.location.hostname.includes("www.erkul.games") || window.location.hostname.includes("ccugame.app")) { + document.body.addEventListener("click", function (event) { + setTimeout(function () { + allTranslate().then(_ => { + }); + }, 200); + }); + } +} + +function WebLocalizationUpdateReplaceWords(w) { + let replaceWords = w.sort(function (a, b) { + return b.word.length - a.word.length; + }); + replaceWords.forEach(({ word, replacement }) => { + replaceLocalesMap[word] = replacement; + }); + allTranslate().then(_ => { + }); + // console.log("WebLocalizationUpdateReplaceWords ==" + w) +} + +async function allTranslate() { + async function replaceTextNode(node1) { + if (node1.nodeType === Node.TEXT_NODE) { + let nodeValue = node1.nodeValue; + const key = nodeValue.trim().toLowerCase() + .replace(/\xa0/g, ' ') // replace ' ' + .replace(/\s{2,}/g, ' '); + if (replaceLocalesMap[key]) { + nodeValue = replaceLocalesMap[key] + } + node1.nodeValue = nodeValue; + } else { + for (let i = 0; i < node1.childNodes.length; i++) { + await replaceTextNode(node1.childNodes[i]); + } + } + } + + await replaceTextNode(document.body); +} + +function traverseElement(el) { + if (!shouldTranslateEl(el)) { + return + } + + for (const child of el.childNodes) { + if (["RELATIVE-TIME", "TIME-AGO"].includes(el.tagName)) { + translateRelativeTimeEl(el); + return; + } + + if (child.nodeType === Node.TEXT_NODE) { + translateElement(child); + } else if (child.nodeType === Node.ELEMENT_NODE) { + if (child.tagName === "INPUT") { + translateElement(child); + } else { + traverseElement(child); + } + } else { + // pass + } + } +} + +function translateElement(el) { + // Get the text field name + let k; + if (el.tagName === "INPUT") { + if (el.type === 'button' || el.type === 'submit') { + k = 'value'; + } else { + k = 'placeholder'; + } + } else { + k = 'data'; + } + + const txtSrc = el[k].trim(); + const key = txtSrc.toLowerCase() + .replace(/\xa0/g, ' ') // replace ' ' + .replace(/\s{2,}/g, ' '); + if (replaceLocalesMap[key]) { + el[k] = el[k].replace(txtSrc, replaceLocalesMap[key]) + } +} + +function translateRelativeTimeEl(el) { + const lang = (navigator.language || navigator.userLanguage); + const datetime = $(el).attr('datetime'); + $(el).text(timeago.format(datetime, lang.replace('-', '_'))); +} + +function shouldTranslateEl(el) { + const blockIds = []; + const blockClass = [ + "css-truncate" // 过滤文件目录 + ]; + const blockTags = ["IMG", "svg", "mat-icon"]; + if (blockTags.includes(el.tagName)) { + return false; + } + if (el.id && blockIds.includes(el.id)) { + return false; + } + if (el.classList) { + for (let clazz of blockClass) { + if (el.classList.contains(clazz)) { + return false; + } + } + } + return true; +} + +InitWebLocalization(); + +function _loadLocalizationData() { + chrome.runtime.sendMessage({ action: "_loadLocalizationData", url: window.location.href }, function (response) { + WebLocalizationUpdateReplaceWords(response.result); + }); +} \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..555fa57 Binary files /dev/null and b/icon.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..9e1b702 --- /dev/null +++ b/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 2, + "name": "星际公民盒子浏览器拓展", + "version": "0.0.1", + "description": "为星际公民网站及工具站提供汉化", + "author": "xkeyC", + "icons": { + "48": "icon.png", + "128": "icon.png" + }, + "permissions": [ + "https://ch.citizenwiki.cn/*", + "storage" + ], + "browser_action": { + "default_icon": "icon.png", + "default_popup": "popup.html" + }, + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [ + "https://robertsspaceindustries.com/*", + "https://www.erkul.games/*", + "https://uexcorp.space/*", + "https://ccugame.app/*" + ], + "js": [ + "core.js" + ] + } + ] +} \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..57608d0 --- /dev/null +++ b/popup.html @@ -0,0 +1,13 @@ + + + + + StarCitizen Web Tools Translation + + + + + TODO + + + \ No newline at end of file