[记录] 无源码给特定应用添加翻译 | 祭夜の咖啡馆
  • 欢迎光临,这个博客颜色有点多

[记录] 无源码给特定应用添加翻译

记录 msojocs 3小时前 2次浏览 已收录 0个评论 扫描二维码
文章目录[隐藏]

前言

维护的两个项目都刚好有翻译需求。

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,
  });

祭夜の咖啡馆 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:[记录] 无源码给特定应用添加翻译
喜欢 (0)
[1690127128@qq.com]
分享 (0)
发表我的评论
取消评论
OwO表情
贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址