前言
维护的两个项目都刚好有翻译需求。
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,
});

