「2023.002|CN」Issues系列:关于 Hexo 及 NexT 主题的多语言支持

新的需求

我的老blog使用的是单语言 en,但发布过英两种语言的 post,且二者内容没有交集。即文章要么是中文的,要么是英文的。

我计划在这个新blog里,尽可能每一篇post都用中日英三种语言分别发布,因此折腾了一下 Hexo 和 NexT Theme 的多语言支持,这里记录一下其中遇到的问题。

Hexo 及 NexT 主题多语言支持的特点

版本

  • "hexo": "6.3.0"
  • "hexo-theme-next": "8.17.1"

相关文档

  • Hexo Docs/ Customization / Internationalization (i18n)
  • NexT Docs / Theme Settings / i18n

相关配置

分别参考 Hexo 和 NexT Theme 的官方文档找到相关配置

Hexo

_config.yml (Hexo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
language:
- en
- zh-CN
- ja

permalink: :category/:lang/:title/
permalink_defaults:
lang: en

i18n_dir: :lang

new_post_name: :lang/:title.md # File name of new posts

# Home page setting
index_generator:
single_language_index: false # show multiple language posts in homepage

NexT Theme

_config.next.yml (NexT Theme)
1
2
# Show multilingual switcher in footer.
language_switcher: true

Issues

Issue#1[部分解决]: 多语言支持未生效

我在 NexT Theme 的官方文档里评论指出了我在配置多语言支持时遇到的问题:

the doc missed a necessary plugin hexo-generator-index-i18n, what's more, in my test you should also create a post with each language, or the related i18n_dir will not be generated. The latter may be a bug.

pwnfan, "NexT User Docs – Starting to Use – Internationalization"comment about problem in multi-language setting

即存在两个问题:

  1. 文档中没有说明 必须安装一个多语言相关的插件 hexo-generator-index-i18n,多语言支持才能使用
  2. 必须发布多语言的文章,相关的多语言文件夹 i18n_dir 才会被创建。

下面分别讨论这两个问题

缺少hexo-generator-index-i18n 插件

解决起来很简单,使用如下命令安装即可:

1
$ npm install hexo-generator-index-i18n --save

i18n_dir 与 post 文章关联问题

首先再详细描述一下这个问题,使用如下命令查看我的 blog 源码(Hexo)目录,如下所示:

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
(base) ➜  Blog tree pwnfan -L 1
pwnfan
├── README.md
├── _config.next.yml
├── _config.yml
├── db.json
├── node_modules
├── package-lock.json
├── package.json
├── public
├── scaffolds
├── source
└── themes

6 directories, 6 files

(base) ➜ Blog tree pwnfan/public -L 1
pwnfan/public
├── 404.html
├── LICENSE
├── about
├── archives
├── atom.xml
├── css
├── en
│ └── index.html # Hexo NexT Theme Home Page(original English)
├── images
├── index.html
├── ja
│ └── index.html # Hexo NexT Theme Home Page(Japanese Translations)
├── lib
├── post
├── search.xml
├── sitemap.txt
├── sitemap.xml
├── tags
└── zh-CN
└── index.html # Hexo NexT Theme Home Page(Simple Chinese Translations)

这些 index.html 用于显示主题对主页面基本组成元素,如菜单等,不同语言下会显示对应语言的菜单。

切换语言可以通过页面最下端的 language_switcher 中实现。

正常情况下,即使不发布任何文章,应该都可以使用 language_switcher 切换语言,以显示不同的主页菜单等基本组成元素。

但实际的情况是,NexT 主题存在bug,若不分别发布各种语言的文章,它就不会生成对应语言的文件夹(即i18n_dir,如 ja)及 index.html,从而就无法使用 language_switcher 进行多语言切换,更不用说使用其他语言的如菜单之类对页面基本组成元素了。

而且 NexT Theme 并不支持在单个 post 里的 Front-matter 里设置多个语言,如果设置了多个且其中包含 en,则会被默认设置为 en,其他语言将被忽略。

这个问题目前没有好的办法解决,只能通过发布各种语言的 post 来解决。

可以给官方提一个 github issue,不过考虑到官方可能不认为这是一个bug而是一种可选的需求,他们只是选择了另一种需求来实现,所以我只是在相关的文档出发表了评论提出了这个问题。

Issue#2[已解决]: 多语言 post assets 包含问题

期望效果

post assets 是 Hexo 的 post 需要引用的资源,其中最常见的资源便是图片了。

如果按照 Hexo 的设计理念,在撰写多语言的 post 时,针对同一篇 blog 文章,我们需要创建多个不同语言版本的 post,如:

1
2
3
4
5
6
# default language is English
hexo new post "About Hexo and NexT Theme Mult-Language Support"
# japanese
hexo new post --lang ja "About Hexo and NexT Theme Mult-Language Support"
# simple Chinese
hexo new post --lang zh-CN "About Hexo and NexT Theme Mult-Language Support"

此时,_posts 目录结构如下:

1
2
3
4
5
6
7
8
source/_posts
├── en
│ └── Issues-Series-About-Hexo-and-NexT-Theme-Multi-Language-Support.md
├── images
├── ja
│ └── About-Hexo-and-NexT-Theme-Mult-Language-Support.md
└── zh-CN
└── About-Hexo-and-NexT-Theme-Mult-Language-Support.md

在这种情况下,如果文章里需要引用图片,可能遇到如下两种情况:

  • 不同语言的 post 都使用同样的图片:最常见的情况
  • 不同语言的 post 使用的某些图片可能不同:比较少见,但是还是偶尔会有这类需求,比如我们制作的图片中包含文字说明,且会根据对应语言制作包含对应语言的文字说明的图片版本。
    • 概括一下这种需求:既存在相同的 assets(如图片),又要支持语言相关的独立 assets(如图片)
    • 引发了一个问题:图片(Assets)文件夹应该如何组织,才能满足上述需求?

问题分析 && 相关信息

我们知道 Hexo 的 Assets 文件夹有两种组织形式:

  • 所有 post 共用同一个 Assets 文件夹 比如 images 文件夹
    • 虽然这是默认的方式,但是我个人不推荐这种方式
    • 当文章多起来时,资源文件如图片都包含在同一个文件夹内,虽然可以通过文件名区分,但是还是会非常的乱
    • 它的优点是能够方便的在不同的地方如 post 中复用之前加入的 assets,但是实际上在这类 blog 应用中,assets 的复用率极低
  • 每个 post 有独立的 assets 文件夹
    • 我比较喜欢这种方式
    • 参考文档
      • Hexo Docs - Asset Folders
      • Hexo Docs - Tag Plugins - Include Assets
    • 具体设置方法:在 Hexo 配置文件 _config.yml 中设置 post_asset_folder: true
    • 这种方式在处理上述的需求 "即存在相同的 assets(如图片),也要支持语言相关的独立 assets(如图片)"时,也会遇到问题
      • 可以粗暴的将相同的图片分别存储在对应语言的 assets 文件夹中,但存在重复文件会浪费磁盘和远程 git 仓库的空间

当我们将设置了 post_asset_folder: true 之后,加上多语言的配置,文件夹结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
(base) ➜  pwnfan git:(dev) ✗ tree source/_posts                             
source/_posts
├── en
│ ├── Issues-Series-About-Hexo-and-NexT-Theme-Multi-Language-Support
│ └── Issues-Series-About-Hexo-and-NexT-Theme-Multi-Language-Support.md
├── images
├── ja
│ ├── About-Hexo-and-NexT-Theme-Mult-Language-Support
│ └── About-Hexo-and-NexT-Theme-Mult-Language-Support.md
└── zh-CN
├── Issues-Series-About-Hexo-and-NexT-Theme-Multi-Language-Support
└── Issues-Series-About-Hexo-and-NexT-Theme-Multi-Language-Support.md

解决方法

为满足 既存在相同的 assets,又要支持语言相关的独立 assets 的需求,可以采用如下方式:

  1. 按照上节推荐,在 Hexo 配置文件 _config.yml 中设置 post_asset_folder: true

  2. 相同的 assets:假如原始 post 的语言为英文,然后分别翻译为中文 post 和日文 post。则可以在中文和日本的 post 中可以直接使用英文 post 的 asset,以避免前文提到的存储重复文件的问题

    • 这里需要专门说明一下,在中文和日文的 post 中引用外部的 assets,不能使用相对路径,而需要使用这种“绝对路径” {% img /en/post/Issues-Series-About-Hexo-and-NexT-Theme-Multi-Language-Support/1.png "image title" %}
      • “绝对路径” 之所以包含引号,是因为它也并不是真正意义上的绝对路径,而是以目录 {blog_source_folder}/public/ 为根目录的绝对路径
  3. 语言相关的独立 assets:直接放在对应语言的 assets 文件夹中即可

Issue#3[已解决]:NexT Theme 在不同页面切换语言的 Bug ⚠️

问题详述

  • 当前 NexT Theme 下在不同页面(主页、post、404、tags等)来回切换语言,可能会出现切换到后的页面返回 404 的情况

期望效果

  • 主页
    • 可以切换多语言
    • 当前 NexT Theme 已支持这种切换
  • 非 post 页面(about、404、tags等)
    • 当前 NexT Theme 切换其他语言后会变成 404,因为它不支持对这类页面的多语言定制,但在生成的切换 URL 中却指定了语言。这是一个bug,需要修复。简单起见期望修改为在这类页面中不可切换多语言,或者使切换无效。
    • 若用户在这些页面内切换了语言,即使我们按照上述的期望方式使切换无效,即让切换后的URL保持不变,但由于NexT的实现机制,仍然会进行页面刷新操作。要想让其不刷新,修改起来比较复杂,简单起见保持不变。
  • post 页面:
    • 可以进行多语言切换,若不存在对应语言的 post,则返回404
    • 当前 NexT Theme 不能很好的支持用户在 config 中定制 permalink 的多言转换。这也另一种bug,也需要修复。

问题分析 && 相关信息

原因是由于 NexT Theme 生成多语言的 URL 的时候存在bug,没有充分考虑当前 Path、permalink 格式、以及多语言的关系导致。

问题代码位于

bug in Line #10 function `i18n_path(language)`hexo-theme-next/layout/_partials/languages.njk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{%- if theme.language_switcher and languages.length > 1 %}
<div class="languages">
<label class="lang-select-label">
<i class="fa fa-language"></i>
<span>{{ language_name(page.lang) }}</span>
<i class="fa fa-angle-up" aria-hidden="true"></i>
</label>
<select class="lang-select" data-canonical="" aria-label="{{ __('accessibility.select_lang') }}">
{% for language in languages %}
<option value="{{ language }}" data-href="{{ i18n_path(language) }}" selected="">
{{ language_name(language) }}
</option>
{% endfor %}
</select>
</div>
{%- endif %}

i18n_path(language) 代码:

bug in i18n_path()hexo-theme-next/scripts/helpers/engine.js
100
101
102
103
104
105
106
107
/**
* Get page path given a certain language tag
*/
hexo.extend.helper.register('i18n_path', function(language) {
const { path, lang } = this.page;
const base = path.startsWith(lang) ? path.slice(lang.length + 1) : path;
return this.url_for(`${this.languages.indexOf(language) === 0 ? '' : '/' + language}/${base}`);
});

解决方法

我在 fork 版本中尝试修复了这个问题:

https://github.com/pwnfan/hexo-theme-next/commit/4a4ce6301ba5e0f7e05eea353c5c771e102dc8bb

这里实现并使用了新的函数 i18n_path_pwnfan,因为上节中提到的 期望效果 包含我的个人主观观点和需求,因此可能不适合给官方 PR,所以只在自己的 fork 仓库里做了修复:

invoke customized function `i18n_path_pwnfan()`hexo-theme-next/layout/_partials/languages.njk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{%- if theme.language_switcher and languages.length > 1 %}
<div class="languages">
<label class="lang-select-label">
<i class="fa fa-language"></i>
<span>{{ language_name(page.lang) }}</span>
<i class="fa fa-angle-up" aria-hidden="true"></i>
</label>
<select class="lang-select" data-canonical="" aria-label="{{ __('accessibility.select_lang') }}">
{% for language in languages %}
<option value="{{ language }}" data-href="{{ i18n_path_pwnfan(language) }}" selected="">
{{ language_name(language) }}
</option>
{% endfor %}
</select>
</div>
{%- endif %}
source code of customized function `i18n_path_pwnfan()`hexo-theme-next/scripts/helpers/engine.js
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
* Get page path given a certain language tag, pwnfan_customized
*/
hexo.extend.helper.register('i18n_path_pwnfan', function (language) {
const { path, lang } = this.page;
let base = path;

if (path === "index.html") { // homepage without language path
base = `${language}/${path}`;
} else if (path === `${lang}/index.html`) { // homepage with a language path
if (language === "en") {
language = "";
}
base = path.replace(`${lang}/`, `${language}/`);
}
else if (!path.endsWith(".html")) { // post page
if (path.startsWith(`${lang}/`)) {
base = `${language}/${path.slice(lang.length + 1)}`;
} else {
base = path.replace(`/${lang}/`, `/${language}/`);
}

}
return this.url_for(base);
});

Issue#4[部分解决]: 如何翻译?

请参考另一篇 post 「2023.003/CN」用 ChatGPT 高效翻译 Hexo 文章

Hexo NexT Theme 多语言支持的另一种方式?

可以看到,Hexo 及 NexT Theme 的多语言支持及包含:

  • 对主页面基本组成元素的多语言支持,如菜单等
  • 多语言 post

而我能想到的另一种可以变相地实现多言语 post 的方式是:

  1. 将 Hexo 及 NexT Theme 设置为只有一种语言,比如 en

  2. 借助 NexT Theme 专有的 Tag Plugins 中的 Tabs,可以变相地实现多语言。 参考代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    {% tabs Multiple Language Post Example %}
    <!-- tab English -->
    Where are you from?
    <!-- endtab -->

    <!-- tab 日本語 -->
    ご出身地はどちらですか。
    <!-- endtab -->

    <!-- tab 中文 -->
    你来自哪里?
    <!-- endtab -->
    {% endtabs %}

    效果如下:

    Where are you from?

    ご出身地はどちらですか。

    你来自哪里?

这种方式存在的问题:

  • 没有对主页面基本组成元素的多语言支持,如菜单等
  • 有两种组织 post 中 Tags 的方式(以支持 en/ja/zh-CN 三种语言为例)
    • post 中只使用 3个 Tags,分别包含不同语言的全部文章内容
      • 优点:代码相对简单清晰
      • 缺点
        • Tabs 有固定的样式框,影响整个 blog 的页面风格
        • ToC 生成存在问题
    • 针对 post 文章中的每一个段落,分别使用独立的 3种语言的 Tags
      • 优点:
        • ToC 可以跟正常工作如初
        • 不影响 blog 的整体页面风格
      • 缺点:
        • 文章段落较多时,需要使用大量的 Tags 标签 {% tabs %},编写繁琐、代码不整洁
        • ToC 只能使用一种语言

技术文档多语言支持的最佳实践

前文记录了为让 Hexo 和 NexT Theme 满足自己对写多语言的 post 的需求,而进行的尝试和思考,显然对我的需求来。

那么我目前为止看过的技术文档的多语言文档中,哪种是最优雅的呢?我另写了一篇文章「2023.004|CN」最佳实践系列:技术文档多语言支持 来讨论这个问题。