开发中关于 "简繁体" 语言配置方案

416 阅读4分钟

1、前言

一天下午,脚踩凉拖鞋,手拿橙c美式,耳戴AirPros Pro(只戴一边,懂得都懂),主屏开着vscode,副屏开着《五亿探长雷洛传:雷老虎》小屏模式,用上高中考试时训练出的斜眼法,快乐摸鱼的时候。

微信突然被“轰炸”,腚睛一看,好家伙,原来是我那活爹PM又来需求了。

PM: 现在公司业务要开拓市场,进军港澳台,所以要给项目加一个可以选用简繁字体的功能,三天之后的发版日就要上线,非常急!

我虽然心里骂骂咧咧,但还是不争气的说:好的,我办事你放心(玫瑰)

2、思考

这不就是多语言配置嘛,一个i18n就搞定啦,洒洒水啦。

3、实操

第一个方案:i18n

当我开始用i18n写多语言的时候,越写越发现不对劲。

一个页面很多文字是写死的,也就是说我需要给这些文字都翻译并保存在繁体的json文件里,同时还要维护 简/繁 体的json文件,最重要的是,这种页面有十几个。。。

image.png

image.png

每次页面有新增字段,都需要在json文件加上对应的繁体文字,还要重新发版,甚至有些从谷歌翻译过来的繁体和实际使用的繁体不一样,所以抛弃了这个方案。

第二个方案: 更换font-family

通过更换font-family,可以实现将简体自动翻译成繁体,不需要手动去维护json文件! image.png 繁体font-family链接:点击这里

由于项目是跑在electron上的,所以需要监听BrowserWindow的 'did-finish-load' 事件,往webContents 注入css 。

webContents.on('did-finish-load', () => {
    webContents.insertCSS(`
      @font-face {
        font-family: 'FanWunHak-R';
        src: url('app://./assets/fonts/FanWunHak/FanWunHak-R.ttf');
      }
      .zh-tw *:not(input, textarea, .fa) {
        font-family: FanWunHak-R,Arial,sans-serif !important;
      }
    `)
    const localConfig = getConfig().local
    if (localConfig.language === 'zh-tw') {
       webContents.executeJavaScript(`document.body.classList.add('zh-tw')`)
    }
}

15行代码,轻松拿捏。非常自信的把包丢在机器上给测试大佬测试,继续愉快的摸鱼。

可能是我想的过于简单,不一会,测试大佬又发消息过来,说:有些字体还是跟实际使用的字体不一样啊。比如:“爱”的繁体是“愛” ,但实际显示“爱”,存在个别字体不对的现象。

不应该啊,难道说FanWunHak这个字体库是不完整的?腚睛一看,好家伙,原来三年都没有更新过了。那这个方案又得放弃。

第三个方案:动态转换

这个方案是在另一个大佬那里看到的,我也是参考了这个方案。

大佬方案链接:点击这里

实际上,动态转换的核心两个函数,这两个函数,其中一个 charPYStr 返回简体的文字,另一个 ftPYStr 返回繁体的文字,通过下标进行文字的替换。

image.png

因为需要在页面渲染出来再进行动态转换,其中也存在接口修改了文字的情况,所以要监听页面dom更新,因此可以通过 MutationObserver 监听dom节点更新,再进行动态转换。

let observer: any
const targetNode = window.document.body;

const config = {
  attributes: false,
  childList: true,
  subtree: true,
  characterData: true,
};

const cb = (mutations: MutationRecord[]) => {
  mutations.forEach(function (mutation) {
    //加上characterData属性,允许观察文本节点的变化
    if (mutation.type === "characterData") {
      observer.disconnect();
      replaceText(mutation.target, 'zh-tw');
      observer.observe(targetNode, config);
    } else {
      replaceText(mutation.target, 'zh-tw');
    }
  });
};

observer = new MutationObserver(cb);

observer.observe(targetNode, config);

其中动态转换的函数是:replaceText,它的具体实现是这样

// 简体字符转为繁体字符;
function simple2traditional(cc: string) {
  let str = "";
  const simplifiedChinese = charPYStr();
  const traditionalChinese = ftPYStr();
  for (let i = 0; i < cc.length; i++) {
    const c = cc.charAt(i);
    if (c.charCodeAt(0) > 10000 && simplifiedChinese.indexOf(c) != -1)
      str += traditionalChinese.charAt(simplifiedChinese.indexOf(c));
    else str += c;
  }
  return str;
}

// 繁体字符转为简体字符;
export function traditional2simple(cc: string) {
  let str = "";
  const simplifiedChinese = charPYStr();
  const traditionalChinese = ftPYStr();
  for (let i = 0; i < cc.length; i++) {
    const c = cc.charAt(i);
    if (c.charCodeAt(0) > 10000 && traditionalChinese.indexOf(c) != -1)
      str += simplifiedChinese.charAt(traditionalChinese.indexOf(c));
    else str += c;
  }
  return str;
}

function replaceText(rootNode: Node, language: string) {
  const converter = language === "zh-cn" ? traditional2simple : simple2traditional;
  function inner(currentNode: HTMLElement, language: string) {
    if (currentNode.tagName === "SCRIPT") return;
    if (currentNode.tagName === "STYLE") return;

    if (currentNode.tagName === "IMG") {
      const altAtt = currentNode.getAttribute("alt");
      if (altAtt) {
        currentNode.setAttribute("alt", converter(altAtt));
        return;
      }
    } else if (currentNode.tagName === "INPUT") {
      const placeholderAtt = currentNode.getAttribute("placeholder");
      if (placeholderAtt) {
        currentNode.setAttribute("placeholder", converter(placeholderAtt));
        return;
      }
    }
    for (const node of currentNode.childNodes) {
      if (node.nodeType === Node.TEXT_NODE) {
        node.textContent = converter(node.textContent);
      } else {
        inner(node as HTMLElement, language);
      }
    }
  }
  if (rootNode.nodeType === Node.TEXT_NODE) {
    rootNode.textContent = converter(rootNode.textContent);
  } else {
    inner(rootNode as HTMLElement, language);
  }
}

由于只需要转换文字,因此把 script、style 等这种标签去掉,不需要转换。

只需要转换 input、img、p、span 等标签,通过判断标签的 nodeType === Node.TEXT_NODE ,就可以知道是不是文字标签或者含有文字。

这样就可以实现一个比较精准的简繁体转换了。

4、总结

方案优势劣势
i18n国际化,支持外文场景工作量大,需要逐个调整
FanWunHak快速、省事只适合简繁体场景,简繁体字体不一致会影响字体图标
动态转换快速、省事,简繁体字体一致只适合用简繁体场景

总之,上面三个简繁体转换方案可以覆盖大部分的场景,如果有其他更合适的方法,可以在评论区交流哦! 我继续去看雷洛传了,真的很好看!