﻿---
title: Notes on Adding i18n to a Hexo Blog
date: 2026-05-22
tags:
  - hexo
  - i18n
excerpt: Design notes on Hexo i18n — filename conventions for language detection, a permalink filter for path-prefix routing, per-language generators, hreflang, and a language switcher.
lang: en
i18n:
  cn: /i18n-in-hexo
  translation: 2
updated: 2026-05-24 22:03:39
---

<script data-swup-reload-script type="module" src="/js/components/tree.js"></script>
<script data-swup-reload-script type="module" src="/js/components/sidenote.js"></script>
<script data-swup-reload-script type="module" src="/js/components/tab.js"></script>

Since this blog went live, the archive page has shown Chinese and English posts jumbled together. Adding i18n to Hexo isn't easy, so the plan kept getting pushed back indefinitely.

I finally put together a first cut, prompted most directly by AnyRouter's Opus model being usable again.

But the deeper reason is interest. Lately I was supposed to focus on [[write_uestc_thesis_using_typst|writing my graduation thesis]], yet I kept getting pulled back into tinkering with the blog. Even at bedtime my mind was still on it, and I enjoyed every minute. The polished UI/UX gives me real satisfaction.

There's also the goal of practicing expression. Looking back at older posts, the same problems keep showing up:

- Foreign words scattered through Chinese prose (for example, the title of this post originally used "i18n" before I switched to "多语言" — internationalization).
- Visible traces of "Europeanized Chinese"[^1].

I've consumed a lot of English content over the years, and as a result I instinctively imitate English patterns when writing Chinese. Stiff phrasing hurts comprehension in obvious ways. Supporting multiple languages on the blog is, in a sense, a way to force myself past that laziness.

There's also a more practical motivation: multilingual support widens the audience. Posting on forums for exposure would be more direct, but I prefer the quieter conversations a blog allows.

## Source layout and routing

### File organization: filename convention

Hexo officially recommends the "directory convention", but I ended up keeping every `.md` flat under `_posts/` and using a `__<lang>` suffix to mark language. This keeps the original and its translation next to each other, which makes them easier to find and edit. Combined with VS Code's File Nesting, you can fold `foo__en.md` under `foo.md` for an even cleaner tree.

<x-tabs>

<x-tab title="Directory convention" active>

<x-tree root="vluv">

- source/\_posts
  - cn/
    - foo.md
    - bar.md
  - en/
    - foo.md
    - bar.md

</x-tree>

</x-tab>
<x-tab title="Filename convention">

<x-tree root="vluv">

- source/\_posts
  - foo.md
  - foo\_\_en.md
  - bar.md
  - bar\_\_en.md

</x-tree>

</x-tab>

<x-tab title="Filename convention + VS Code File Nesting">

<x-tree root="vluv">

- source/\_posts
  - foo.md
    - foo\_\_en.md
  - bar.md
    - bar\_\_en.md

</x-tree>

Add the following to `settings.json`:

```json
"explorer.fileNesting.patterns": {
  "*.md": "$(capture)__en.md"
}
```

</x-tab>

</x-tabs>

### Routing: path prefix

I went with the `/en/foo/` path-prefix scheme, which is friendlier to SEO and caching. The mapping:

| Role                  | Input filename | Output URL   |
| --------------------- | -------------- | ------------ |
| Default-language post | `foo.md`       | `/foo/`      |
| Translated post       | `foo__en.md`   | `/en/foo/`   |
| Default-language page | `about.md`     | `/about/`    |
| Translated page       | `about__en.md` | `/en/about/` |

For **Posts**, I use the `permalink: :i18n_path:name` template and register a low-priority `post_permalink` filter that injects `i18n_path` (empty string for the default language, `"en/"` for English — both derivable from the filename) and rewrites the slug (stripping the `__en` suffix from the filename).

**Pages** are few, so I just declare `permalink` directly in front-matter — for example, `permalink: /en/links/`.

## Isolating Archive, Tag, and search

- **Tag / Archive**: Run the generator once per language, feeding it only that language's posts, to produce two fully independent sets of tag and archive pages.
- **Search**: At build time, emit `/content.json` and `/en/content.json` separately. Each language's pages load their own index, so results never cross languages.

`sitemap.xml`, `atom.xml`, and similar resources are not split by language.

## SEO

To help search engines correctly identify the relationship between an original and its translation, I declare an `i18n` mapping in front-matter, like this:

<x-tabs>

<x-tab title="Chinese original" active>

```md langya_terrace.md
---
title: 琅邪台游记
date: 2026-03-13
excerpt: 孤立特显，出于众山上，下周二十余里，傍滨巨海
tags:
  - Essay
  - 青岛
i18n:
  en: /en/langya_terrace
updated: 2026-05-23 00:30:39
---

Original content...
```

</x-tab>
<x-tab title="English translation">

```md langya_terrace__en.md
---
title: Langya Terrace Travel Notes
date: 2026-03-13
excerpt: Standing alone and prominent above all other hills, with a circumference of over twenty li at its base, overlooking the great sea
tags:
  - Essay
  - 青岛
lang: en
i18n:
  cn: /langya_terrace
  translation: 2
updated: 2026-05-23 00:30:59
---

Translated content...
```

</x-tab>

</x-tabs>

Based on the `i18n` field, the theme automatically generates `{html} <link rel="alternate" hreflang>` tags pointing to the equivalent URL in each language:

```html
<link rel="canonical" href="https://vluv.space/langya_terrace/" />
<link
  rel="alternate"
  hreflang="zh-CN"
  href="https://vluv.space/langya_terrace/"
/>
<link
  rel="alternate"
  hreflang="en"
  href="https://vluv.space/en/langya_terrace/"
/>
<link
  rel="alternate"
  hreflang="x-default"
  href="https://vluv.space/langya_terrace/"
/>
```

`{html} <html lang>` and `og:locale` also switch automatically with the page's language.

## Interaction design

- **Language switch in the nav bar**: Pages like Archive/Tag/Links/About jump directly. Posts work the same way, but since not every post has a translation, switching language on a post may need to fall back to a toast notification.
- **Translation provenance marker**: Post detail pages display a translation status based on `i18n.translation`: default is original, `1` means LLM-translated and reviewed, `2` means LLM-translated and unreviewed — useful for readers to gauge the text.

![Interaction design](https://assets.vluv.space/2026-05-24-Untitled.key-000042.avif)

[^1]:
    [Europeanized Chinese](https://baike.baidu.com/item/%E6%AC%A7%E5%8C%96%E4%B8%AD%E6%96%87/9810789#reference-2) is a grammatical phenomenon that appeared in modern Chinese under the influence of Indo-European languages (especially English). It mainly shows up as sentence structures that drift away from traditional Chinese expression: overly complex sentences, overuse of the passive voice, excessive use of structural particles like 的 and 地, abuse of prepositions like 通过 and 作为, and the habit of decomposing simple verbs into "weak verb + abstract noun" compounds (e.g. "作出贡献" replacing "贡献很大"). It is most often seen in literal translations and formal written language.

    The phenomenon traces back to the New Culture Movement, when writers like Lu Xun and Qian Xuantong argued for reshaping Chinese thought by directly importing European grammar. Lu Xun explicitly advocated "literal translation of European syntax" as a way to renew the language. The translator Si Guo, in *Translation Studies*, criticized such writing as "barely intelligible, but certainly not Chinese", arguing that Europeanized Chinese violates the essence of the language.

    In the 1980s, Yu Guangzhong's essay "How to Improve English-Style Chinese?" went further, dissecting the tendency toward "verbose and stiff" Chinese expression. He criticized redundant prepositions (e.g. 由于, 关于), inappropriate conjunctions (e.g. 及), and the overreliance on 的 with adjectives — all Western-influenced traits — and insisted on preserving the natural rhythms of Chinese.
