Featured image of post 纯 JavaScript 双语化小记

纯 JavaScript 双语化小记

暴力达到极致,也是一种艺术。——纪念这次各种花式抓耳挠腮之后的有所收获

双语化是一个很酷的功能,如果能在我的个人博客上实现它,那绝对是狂拽酷炫屌炸天。对于已经熟悉了 HTML 和 CSS 的我来说,JavaScript 是「最后一块拼图」,于是,我从 2021 年 12 月 11 日开始自学 JavaScript,目前已经看完了初级课程。在有了那么一丢丢基础以后,我开始着手双语化代码的构建。~~其实本来是想做国际化的,也就是一个通用键对应各种语言的各种不同值,可惜因为我太菜,最终只能从实用角度考虑做成了中英切换。~~进化过了,现在已经不限语言数量了。

我的博客模板 Jekyll 用了 Django 模板语言,今天一瞧,Django 是有 i18n 能力的,而且作为比较「原生」的语言,写进去也挺和谐。但问题是,它在功能很强大的同时也意味着较高的学习成本,就那么一点点时间我根本整不明白。于是放弃。

然后我就开始谷歌关键字 Jekyll i18n,搜出来了三四个方案,要么是作为 Jekyll 插件与其他插件兼容性不好(比如 Jekyll Multiple Languages Plugin,搞掉我几个钟头但是最终还是遗憾放弃);要么是年久失修,我看着就心里慌,不敢用;再者就是文档讲得不清不楚,看得一头雾水干脆关闭浏览器标签页了事……在碰壁了一整个上午和下午以后,我决定还是追随初心,用纯粹的 JavaScript 来实现我的目标。

在一开始,我并没有本地预览的需求,我知道我这个博客是可以 配置本地预览环境 的,但是之前一直没有弄,今天实在是没办法了,被迫排除困难整了一个,不然代码更新频次太低,根本没办法做事。中途还遇到了一些问题,比如,我这个模板涉及到一些额外的 Jekyll 插件,需要额外运行几行安装代码,不然报错;还有个报错居然是因为 Ruby 版本太高,于是卸载掉重新装了 2.7.5,整个过程重新走一遍,属实酸爽。

但是,**但是!**付出是值得的!这篇文章的出现,意味着界面整体双语化的彻底完成,我兴奋得只想拍肚皮,有需要自己看就好,这里讲一下实现思路。

原理

实现原理非常简单粗暴,就是遍历+字符串替换,没,别,哒。但是除了替换,还有另外三个重要的问题需要解决:

  1. 方便快捷通俗易懂的全局双语切换器;
  2. 在页面刷新、跳转以后仍然记住浏览者选中的语言并自动完成替换工作;
  3. 如果不同语言对应的超链接有差异,就需要做自动跳转,否则会「张冠李戴」。

如何遍历

由于不能在底层完成双语化,则必须要做遍历。而且很多内容是动态生成,根据某一处填写的字符串在多处复用的,比如说博文的标题,它会出现在首页、分类、标签各页面中,所以如果针对这类页面去分别做成独立的双语文件(比如 types-zhtypes-en),绝对是事倍功半的,反倒是统一用 JavaScript 全局替换比较省事。

遍历的方案也不止一种。最开始,我是用的 HTML 标签匹配,由于需要双语化的文本存在于 <h2><a><li> 等各类标签中,于是 JavaScript 的命名空间就变得相当混乱,遍历替换函数所需的变量也凭空多了一个。这好吗?这不好。所以解决方案是什么?答案其实很简单,用 <span class="keys"></span> 将所有需要双语化的文本套起来,并且统一用 keys 这个类来管理,这下它们马上就有了共性,而变量的声明赋值也变得简洁了。

注意,因为思路是遍历,所以遍历替换代码必须要等到页面代码全部加载完成以后再执行,所以我用了 window.onload 来保证这一点。但这样也带来一个问题——在切换页面时,如果所选语言是英文,会短暂地显示中文,然后快速地全部替换成英文。这也是两害相权取其轻的决策了。因为这样的实现思路是一定会有延迟的(当然也可能没有,我这样说是因为我所知比较少,如果有无延迟的解法也请告诉我),那么是显示中文好,还是显示一个通用性的键值(类似于 search_posts)好,答案是很明确的。所以,替换的思路就是遍历中文替换英文;当语言又指定回中文时,再替回来。

由于文本替换的代码复用度极高,我将之做成了名为 replace 的函数,变量只有两个(匹配关键字和用于替换的字符串),还是比较简洁的,形如 replace("Bath, Songs, and Home", "浴沂咏归");,可读性还行。

一位好兄弟告诉我,可以采用遮罩的方式,在页面完成前先挡起来,加载完毕以后再显示,于是我使用了 本文 的代码构建了逻辑,从 这里 嫖了转圈圈代码。此外,还需要保证圈圈转动的时候页面不能滚动,于是又嫖了 这里 的代码,七拼八凑完成了这项功能。

另一个实现思路与局限性

21 日早上想到另一个实现思路,给每个 <span class="keys"> 一个独立的 id,这样就能根据 id 直接指定内部的 HTML 内容。这样做的局限性在于:

  1. 由于也是文本替换,所以一样会有一个小延迟,还是需要设置默认文本,或者干脆留空,但会和其他自动生成的文本不同步,也不妥;
  2. 动态代码批量生成的内容无法获知确切的 id,会造成代码逻辑的割裂。

因此,该思路不可行。

如何构建全局双语切换器

毫无疑问,放置着我头像的导航栏是不随内容变更而变化的,宽屏状态下,它在左边;当屏幕缩窄,它就会跑去上边。所以,导航栏存在一个恒定位置,那就是左上角,将切换器放在这个位置正合适。

切换器可以以多种形式出现,下拉菜单、单选框、按钮组……我是菜鸡,就用超链接来做了。

但是它同样要起到指示状态的作用,当语言切换到中文,则中文对应的超链接应该变灰且不可点击;切换到英文亦复如是。为了达到这个目标,我构建了一个名为 btnChange 的函数,专门用于切换超链接的外观。当它检测到当前语言选中了中文,就将中文变灰。而这个函数会在页面刷新和按下语言切换链接时执行,这样就起到了指示切换两个作用。

之后,我又更新了一次,将原本的文字式切换器换成了国旗式的,看起来更加直观。

如何记住浏览者选中的语言

说来也巧,这个需求显然是很普遍的,所以有个属性叫 window.localStorage,能够在浏览者本地存储键值对,我一瞧,大腿一拍:「好,就是你了!」于是,刷新、跳转也不影响向浏览者显示他所需要的语言了。

在写这篇博文的时候,我突然想到,之前测试过程中,我已经摁了无数次语言切换链接,也就是说,我没有测试到语言指示器为空的情形!于是,我开了一个隐私模式的窗口,果然,出了问题,左上角的切换器并没有一个灰一个亮,而是兄弟俩都「闪闪发光」。我顿时怒从心头起,恶向胆边生,反手就是一行 if(!ls.lang){ls.lang = "cn";} 插进去,给他强行塞了个默认的中文状态(也就是说,如果 ls 对象的 lang 属性不存在,就强行设置为中文),问题确实解决了,但是我转念一想啊,这太粗暴了!我如果不是中文用户呢?看到的还是中文天书,确实不够友好啊,所以我参考了 这篇教程 根据浏览者的浏览器语言设置做了判定,但凡非中文浏览器,一律默认显示英文,好!鼓掌!

如何避免张冠李戴

呐,举个例子。我的个人介绍页面的正文文本在别处不会复用,而且内容较多,如果做成文本替换,显得很傻,所以我另外写了一个 about-en.md,这样我还能方便地使用 Markdown 排版,岂不美哉?

但问题在于,这个英文的个人介绍页面要如何通过去。如果做成中文文本里边一个链接通英文,英文再加一个链接通回来,也很傻。

所以干脆做个超链接地址替换,在英文界面下,导航栏里「About」的超链接就是直接指向英文的介绍页;而如果切换到中文,就再换回来,这样就保证浏览者这边无需做任何额外的操作,就能看到自己最熟悉的语言。但问题彻底解决了吗?并没有。

假设我在英文状态下浏览到 about-en,如果不做特殊设计,我在这个页面切中文,网页地址是不会变化的,也就是说,变成了中文界面下看英文文章,这好吗?这不好。于是再加一个操作:如果英文下浏览器地址栏指向了中文的介绍页,就做一个自动跳英文的动作;反之亦然,这样就实现了统一了。

这边要留一个 TODO,到时候需要做一个方便的函数,用来匹配和替换地址栏中的语言关键字,这样哪怕双语文章多起来,也不需要在代码里一个个给它们添加。

任务已经完成,将原本适用范围较窄的、仅针对「About」的替换代码替换成了基于正则表达式的,自动感应帖子地址是否含有语言关键字的替换方式。正则表达式为:/\b-zh\/$/,意思是,以 -zh/ 结尾的字符串。将这部分内容替换成 -en/ 就实现了同文章双语版本的互切。于是,对帖子的命名格式也需要微调:原本是 yyyy-mm-dd-post_title,现在需要在尾部额外加上语言关键字,变成 yyyy-mm-dd-post_title-language,即可,需要调整的地方也不多。可以说实现了功能和成本的平衡。

英文比较坑的地方在于,如果标题带双引号,会和既有的、包裹字符串用的双引号冲突,所以在命名英文帖子时要注意加转义符号,否则就会导致博文文件无法被识别到(别问我怎么知道的)。

此外,需要根据当前的语言调整网页标题,也就是 <title> 的内容,原理同上,也是正则匹配字符串替换。

再次庆祝,溜了溜了。

如何使它具有更强的扩展性?

之前的原理是双语文本互换,但是这样并不方便,类似的文本要写两遍,只不过是方向反过来,这就很不优雅。如果要支持更多的语言,重复简单代码的数量就会呈几何式增长,这可不行!

于是,我们转变一下思路。中 → 英的代码不变,但切回来的原理,可以不是替换,而是修改语言关键字后直接刷新,显露 HTML 原本定义好的那些文本,于是这部分 replace 代码直接砍掉一半。

如果要支持第三种语言,也很简单。如果当前处在英文状态,想要切换到第三种语言,只需要先初始化到中文,然后再单向切过去就行了,也就是说,以中文为桥梁,连通所有其他语言,代码上也会很简洁。

你也想用这个?

这里有个我制作的开源 简化版 供你使用。

Written by ZexWoo. All rights reserved.
主题 StackJimmy 设计 | 使用 Hugo 搭建