前言
维护的两个项目都刚好有翻译需求。
bilibili-linux
-> 中译英
fiddler-everywhere-enhance
-> 英译中
主要是没有源码,一开始想着AST翻译,发现这样是死的,不能动态切换;
然后想弄一个能动态切换通用性强的方案,于是在找翻译的方法。
困难点
- 一些元素是在使用过程中出现的。
- 一些元素是人为创建的,不能被翻译。
解决
一开始试过拦截createTextNode
这些方法,但是一通弄下来感觉代码有点复杂。。。
后面想到新的方案,监听dom元素的变化,然后把Text过滤出来,进行翻译。
对于识别人员创建的元素,通过对node进行运算,符合条件才进行翻译。
const i18n = { "Report an Issue": { condition: (node) => node?.parentNode?.className === "button__text ng-star-inserted", en: "Report an Issue", zhCn: "报告问题", }, }; const regExpI18n = [ { regExp: /Timings \((.*?)\)/, en: ($0, $1) => `Timings (${$1})`, zhCn: ($0, $1) => `时序 (${$1})`, }, ]; // TODO: 用于切换语言时更新 const node2keyword = new Map(); let lang = "zhCn"; window.switchLanguage = async (newLang) => { if (newLang === lang) return; console.info("switchLanguage", newLang); lang = newLang; for (const [node, keyword] of node2keyword.entries()) { if (i18n[keyword]) { const translation = i18n[keyword][lang] || keyword; if (node.nodeType === Node.TEXT_NODE) { node.nodeValue = translation; } else if (node.nodeName === "INPUT" && node.placeholder) { node.placeholder = translation; } else if (node.attributes && node.attributes.title) { node.attributes.title.nodeValue = translation; } else if (node.dataset && node.dataset.placeholder) { node.dataset.placeholder = translation; } } } }; const checkCondition = (node, condition) => { if (typeof condition === "function") { return condition(node); } return true; }; const translateRegExp = (text) => { for (const item of regExpI18n) { if (item.regExp.test(text)) { return text.replace(item.regExp, item[lang]); } } return text; }; /** @param {Node} node */ const translate = (node) => { // console.info('translate', node, node.nodeType, node.nodeName, node.nodeValue) if (node.attributes && node.attributes.title) { // 处理title属性 const title = node.attributes.title; const translation = i18n[title.nodeValue]; if (translation && checkCondition(node, translation.condition)) { node2keyword.set(title, title.nodeValue); title.nodeValue = translation[lang] || title.nodeValue; } } if (node?.dataset?.placeholder) { // 处理data-placeholder属性 const translation = i18n[node.dataset.placeholder]; if (translation && checkCondition(node, translation.condition)) { node2keyword.set(node, node.dataset.placeholder); node.dataset.placeholder = translation[lang] || node.dataset.placeholder; } } if (node.nodeType === Node.TEXT_NODE) { if (!node.nodeValue) return undefined; if (!i18n[node.nodeValue]) { // 正则处理 const translatedText = translateRegExp(node.nodeValue); if (translatedText !== node.nodeValue) { node2keyword.set(node, node.nodeValue); node.nodeValue = translatedText; } return; } if (!checkCondition(node, i18n[node.nodeValue].condition)) return; node2keyword.set(node, node.nodeValue); node.nodeValue = i18n[node.nodeValue][lang] || node.nodeValue; } else if (node.nodeName === "INPUT") { // 处理input元素的placeholder if ( node.placeholder && i18n[node.placeholder] && checkCondition(node, i18n[node.placeholder].condition) ) { node2keyword.set(node, node.placeholder); node.placeholder = i18n[node.placeholder][lang] || node.placeholder; } } }; const observer = new MutationObserver((mutations) => { // console.info('[load]: MutationObserver', mutations); mutations.forEach((mutation) => { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { translate(node); // 递归处理子节点 const walker = document.createTreeWalker( node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT ); while (walker.nextNode()) { translate(walker.currentNode); } } else if (node.nodeType === Node.TEXT_NODE) { translate(node); } }); } else if (mutation.type === "attributes") { // 处理属性变化 translate(mutation.target); } }); }); if (!document.body) { console.warn( "MutationObserver: document.body is not ready yet, waiting for it to be available." ); document.addEventListener("DOMContentLoaded", () => { console.info( "MutationObserver: document.body is now ready, starting to observe." ); observer.observe(document.body, { childList: true, subtree: true, attributes: false, }); }); return; } console.info( "MutationObserver: document.body is ready, starting to observe." ); observer.observe(document.body, { childList: true, subtree: true, attributes: false, });