使用hexo构建一个真·多语言博客网站

背景

双曲线Minecraft团队要做一个网站,同时有四门官方语言:中文,英文,hio语和hez语。hexo自带国际化功能,但是只支持生成多语言版本的文章。据说有一个名字叫做hexo-generator-i18n的项目有更高级的多语言功能,但是说实话,文档写得太晦涩了,CRS没读明白。(如果有人看得懂,可以在评论区里留言它都能实现什么)于是CRS准备自己动手实现。

概述

只需要创建四个hexo工作目录,在四个工作目录里分别生成网站,最后把public目录整合到一起即可。

由于Windows不支持Symbolic Link,因此,CRS把工作目录转移到了Linux上。

创建链接

不同语言的网站共用相同的外部库,模板和主题,但是使用不同的源和配置文件,因此首先,CRS把中文网站的目录定为主目录,其他每个目录下的node_modules/scaffolds/themes/package.json都建立一个指向主目录的symlink。而source/目录和配置文件则需要分别设置。

graph LR

    subgraph Main Directory
        中文目录
    end

    subgraph Slave Directories
        英语目录
        hio语目录
        hez语目录
    end

    外部库/模板/主题--存储于-->中文目录--生成-->主网站
    中文源和配置文件--存储于-->中文目录
    英语源和配置文件--存储于-->中文目录
    hio语源和配置文件--存储于-->中文目录
    hez语源和配置文件--存储于-->中文目录

    subgraph Generate and Merge
        英语目录--生成-->英语网站--合并到-->主网站
        hio语目录--生成-->hio语网站--合并到-->主网站
        hez语目录--生成-->hez语网站--合并到-->主网站
    end
    subgraph Source and Config
        中文源和配置文件
        英语目录--链接到-->英语源和配置文件
        hio语目录--链接到-->hio语源和配置文件
        hez语目录--链接到-->hez语源和配置文件
    end
    subgraph Common
        英语目录--链接到-->外部库/模板/主题
        hio语目录--链接到-->外部库/模板/主题
        hez语目录--链接到-->外部库/模板/主题
    end
lib/initlink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/env sh
copy_list=('node_modules' 'scaffolds' 'themes' 'package.json' 'package-lock.json')
i18n_list=($(./lib/i18nList))
chmod +x ./lib/*
for raw in ${arr[@]}
do
entrie=(${raw//:/ })
lang=${entrie[0]}
dir=${entrie[1]}
mkdir -p $dir
for file in ${copy_list[@]}
do
ln -sfT ../hsmc/$file $dir/
done
ln -sfT ../hsmc/source_$lang $dir/source
ln -sf ../hsmc/_config.$lang.yml $dir/_config.yml
ln -sf ../hsmc/_config.icarus.$lang.yml $dir/_config.icarus.yml
done

这样一来,从目录就有了这样的目录结构:

hsmc_en/
1
2
3
4
5
6
7
8
9
_config.icarus.yml -> ../hsmc/_config.icarus.en.yml
_config.yml -> ../hsmc/_config.en.yml
db.json
node_modules -> /home/crs_16423/hsmc-i18n/hsmc/node_modules
package.json -> ../hsmc/package.json
package-lock.json -> ../hsmc/package-lock.json
scaffolds -> /home/crs_16423/hsmc-i18n/hsmc/scaffolds
source -> ../hsmc/source_en
themes -> /home/crs_16423/hsmc-i18n/hsmc/themes

增加语言切换器

icarus主题不含语言切换器功能,所以CRS为双曲线Minecraft团队制作了一个含有语言切换器的修改版icarus主题

  • 对于文章和页面,在文章元信息部分增加语言切换配置,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    title: 沼都地铁二号线
    date: 2021-08-30 13:30:44
    tags: [地铁线路,采用第二代地铁建设标准的地铁设施]
    category: 城市轨道交通线路
    i18n:
    zh-CN: 沼都地铁二号线
    en: Zhodue Metro Line 2
    hio: Dik-2 Lin de Metro Zhodu
    hez: 沼都地铁第二线

    其中,配置项的值是该文章在另一个语言中的标题。

  • 对于主页,归档页,分类页和标签页,由于在不同语言中页面路径相同,因此不需要加以特殊配置。

  • 对于标签和目录,则需要新增一个配置文件专门用于配置不同语言中同一分类或者标签的不同名称。

    translation_index.yamllink
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    translation_index:
    - type: category
    i18n:
    zh-CN:
    en: Meta
    hio: Myatafarenolle
    hez: 先ㄩㄈ序
    - type: category
    i18n:
    zh-CN: 城市轨道交通线路
    en: "Urban Rail Transit Line"
    hio: "Lin de Durutoluci Damineohav zuin Negōtoli"
    hez: "城市轨ㄡ路ㄌㄧ交ㄖㄨ通ㄌㄨㄒㄩ线"
    - type: tag
    i18n:
    zh-CN: 地铁线路
    en: "Metro Line"
    hio: "Lin Metro"
    hez: "地轨线"
    - type: tag
    i18n:
    zh-CN: 采用第二代地铁建设标准的地铁设施
    en: "Metro Facilities Using the Second-generation Metro Construction Standard"
    hio: "Etūmak Metro io Norme Dik-2 Generacion de Konstrukcion Metro"
    hez: "第二代地轨建设标准用之地轨做ㄜ件"

    然后在生成时将其转换为映射:

    lib/generateTranslationIndexlink
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/bin/node
    require('./checkOs');
    const yaml = require('js-yaml');
    const fs = require('fs');

    const translationIndex = yaml.load(fs.readFileSync('translation_index.yml')).translation_index;
    let translationMap = {category:{},tag:{}};
    for(let entry of translationIndex){
    for(let lang in entry.i18n){
    if(!translationMap[entry.type][lang])translationMap[entry.type][lang]={};
    translationMap[entry.type][lang][entry.i18n[lang]]=entry.i18n;
    }
    }
    fs.writeFileSync('translationMap.json',JSON.stringify(translationMap));

语言切换器的实现如下:

language.jsxlink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const { Component } = require('inferno');
const path = require("path");
const translationMap = require('../../../../translationMap');
const S = require("string");

function getLanguageDisplayName(s,languageDisplayNameMap){
return languageDisplayNameMap&&languageDisplayNameMap[s]?languageDisplayNameMap[s]:s;
}
function getPath(lang,s,defaultLang){
if(s==="[currentLanguage]")return s;
if(lang==defaultLang)return path.join("/",s);
else return path.join("/",lang,s);
}
function parseName(s){
return S(s).latinise().replaceAll(" ","-").toString();
}

module.exports = class Language extends Component{
render() {
const {page,config,helper} = this.props;
const currentLanguage = page.lang || page.language || config.language;
const defaultLanguage = config.default_language;
let languageIndexedList = {};
let languageDisplayNameMap = config.lang_display_name;
if(helper.is_page() && page.i18n){
languageIndexedList = page.i18n;
}else if(helper.is_post() && page.i18n){
Object.keys(page.i18n).forEach(lang=>languageIndexedList[lang]=path.join('posts',page.i18n[lang]));
}else if(helper.is_home()){
Object.keys(languageDisplayNameMap).forEach(lang=>languageIndexedList[lang]='');
}else if(helper.is_categories()){
Object.keys(languageDisplayNameMap).forEach(lang=>languageIndexedList[lang]='categories');
}else if(helper.is_tags()){
Object.keys(languageDisplayNameMap).forEach(lang=>languageIndexedList[lang]='tags');
}else if(helper.is_archive()){
Object.keys(languageDisplayNameMap).forEach(lang=>languageIndexedList[lang]='archives');
}else if(helper.is_category()){
let rawMap = translationMap.category[currentLanguage][page.category];
if(rawMap){
Object.keys(rawMap).forEach(lang=>languageIndexedList[lang]=path.join('categories',parseName(rawMap[lang])));
}
}else if(helper.is_tag()){
let rawMap = translationMap.tag[currentLanguage][page.tag];
if(rawMap){
Object.keys(rawMap).forEach(lang=>languageIndexedList[lang]=path.join('tags',parseName(rawMap[lang])));
}
}
languageIndexedList[currentLanguage]="[currentLanguage]";
return <span class="navbar-item language" title="语言 Language Ryannūs 言语ㄜㄙ">
<i class="fas fa-language" style="margin-right: 0.5rem;"></i>
<select>
{Object.keys(languageIndexedList).sort((a,b)=>a==currentLanguage?-1:b==currentLanguage?1:0).map(lang=>{
return <option data-url={getPath(lang,languageIndexedList[lang],defaultLanguage)}>{getLanguageDisplayName(lang,languageDisplayNameMap)}</option>
})}
</select>
</span>
}
}

请注意其中的parseName函数。CRS发现hexo生成的标签和分类的目录名称不是标签和分类的原名,而是加以转换后的名称。已经发现的变化有:将名称的拉丁字母去帽,将空格转换为连字符。CRS不知道hexo具体的实现,于是自己重新实现了一遍已知的转换。

语言切换器位于导航栏。效果如下:

至此,便实现了一个完整的语言切换体系。

命令行工具

因为需要生成多语言网站,所以CRS还增加了一键增加新文章(正在开发中),一键生成,一键清除等实用库。

(未完待续)

作者

李星烨

发布于

2021-09-05

更新于

2021-09-05

许可协议

CC BY-NC-SA 4.0

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×