Go Multilingual Mode in Hugo

紀錄用 Hugo site generator 建立多語言網站時所遇到的問題,與過程中相關的學習內容,包含 Hugo render 過程、source code trace 等。

預備知識

The flow of Hugo Render

Hugo 是一個 Site Generator,那他是怎麼產出這些 static html 的呢?首先程式會先遍歷在 content folder 底下所有的 folder 和 file,並且組成一個 contentTree (題外話, contentTree 是採用 radix 資料結構)。然後再把這些 content node 根據 node type (e.g. type 可能是 sections or taxonomies) 去對應到 layout folder 內的 template,接著 render 出最後的 html 頁面。(也可參閱 hugolib/site.go 由 Hugo 開發者所寫的簡易流程)

從上述可以大概知道整個 Sites 的基本物件內容:

hugo

Multi-language Sites

有了前面的概念之後,就可以知道 multi-language site 是怎麼產出的了,它其實就是 render 成不同語言的獨立頁面,然後透過 path 來分類出當前的頁面是哪個語言,其目錄結構如下:

hugo

而就是因為這樣的目錄結構,我們才可以簡單地在 path 加上 /zh-tw/ ,就切換到繁中語言網站。

Langs 相關 Variable / Function 的說明與使用範例

Hugo 提供 Languages 相關的 variable 和 function,讓我們在寫 template 的時候拿來使用,所以下面會介紹幾個常用的 variable 和 function,並說明適當的使用時機。

Site Variables

Site 有蠻多 Language 相關的 variable 可以使用,如下

.Site.IsMultiLingual
.Site.Language.Lang
.Site.Language.LanguageName
.Site.Language.Weight
.Site.Language
.Site.LanguageCode
.Site.LanguagePrefix
.Site.Languages

其中,這些 variable 的 output 大多與 hugo lang configuration 有關,例如:

languages:
  en:
    languageName: "English"
    weight: 1
  tw:
    languageName: "繁體中文"
    weight: 2

對應的 .Site.Language.Lang 就是 tw;而 .Site.Language.LanguageName 就是 繁體中文。我會建議這個命名最好與其他 resources 設定一樣的命名規則,舉例來說,如果語言要顯示出對應的 icon,而 icon 的class name 是 tw,那麼這個名稱也要設定成 tw,如此一來在寫 template 的時候,就可以很方便的寫成:

<p>
  <span class="flag-icon-{{ .Site.Language.Lang }}"></span>
  .Site.Language.LanguageName
</p>

(Note: Page 中也有 .Language variable,所以寫成 .Language.Lang 也是可以的。)

Page Variables

Page 是 template 的 default context variable,而它所提供的 lang 相關 variable 如下:

.IsTranslated
.Translations
.TranslationKey
.Language
.Sites.First

.Language 就不提了,因為跟 .Site.Language 一樣,比較重要的是 .IsTranslated.Translations,官方文件在介紹時會提及一個範例:

{{ if .IsTranslated }}
<h4>{{ i18n "translations" }}</h4>
<ul>
    {{ range .Translations }}
    <li>
        <a href="{{ .Permalink }}">{{ .Lang }}: {{ .Title }}{{ if .IsPage }} ({{ i18n "wordCount" . }}){{ end }}</a>
    </li>
    {{ end }}
</ul>
{{ end }}

.IsTranslated 是說這個頁面有沒有提供其他語言版本, e.g. 如果只有 about.md ,那就是 false ,反之如果有 about.md 和 about.tw.md ,那就是 true。IsTranslated 在各 content page 會因為你提供的文件而有所差異,不過在 home page 基本上有提供語言設定的都會是 true。

.Translations 是列出當前頁面所有翻譯版本的 Page,它 return 的結果會是 Page array,因此才會需要使用 range。舉例來說,我們提供 about.md, about.tw.md, about.ja.md 三個語言版本的頁面,而當前頁面是 en 版本,所以 .Translations 會回傳 tw, ja 兩語言的翻譯 Page。

而在 Page 迴圈當中,印出各 Page 的 language code,因此產出結果會變成:

<ul>
    <li>
        <a href="www.example.com/tw/about">tw: 繁中版本字數20</a>
        <a href="www.example.com/ja/about">ja: 日文字數30</a>
    </li>
</ul>

另外,.Sites.First 也是另一個可能會用到的 variable,.Sites.First 會回傳第一個語言的網站內容,用上述的例子來說,在 tw Site 使用 .Sites.First,結果就會是 en Site。通常我們會用在需要取得其他語言網站內容,後面會有使用實例。

Function

Language 相關的 function 包含:

lang.Merge
lang.NumFmt

其中,lang.Merge 是蠻容易搞混的 function,所以特別提到它的用法和範例。根據官網文件, lang.Merge 的用途是結合其他語言的 page,讓沒有翻譯到的內容也可以使用其他語言來呈現。例如,當前 Site Language 是 tw,而某一個 Page 只有英文版本,這時候就可以使用 lang.Merge 功能來呈現英文內容。

官網範例如下:

{{ $pages := .Site.RegularPages }}
{{ range .Site.Home.Translations }}
  {{ $pages = $pages | lang.Merge .Site.RegularPages }}
{{ end }}

.Site.RegularPages 是指特定語言 Site 的 leaf pages。

補充:什麼是 RegularPages?

RegularPages 簡單來說就是 *.md 這些 file 所產生的 page object,所以又被稱作 leaf page。在 Hugo 的世界中,萬物都是 Page,像是 section, taxonomy 這些也是以 Page 的型態存在 (Page 真正的型別是 Go interface),但是真正包含文字內容的是 RegularPages,所以才會看到 template 中會有 .Site.RegularPages 這類的變數出現。

接著,

{{ range .Site.Home.Translations }}

如同上面提到, .Translations 是列出所有翻譯過的 Page,但是為什麼要特別指定 .Site.Home 呢?主要原因是 .Site.Home 意指網站的 home page ,而 home page 是每個翻譯版本都需要的 Page,所以透過 home page,才能真正的取得所有翻譯版本的 Page。如果只用 .Translations 的話,有可能當前 Page 並沒有其他翻譯版本,那就會回傳空 array。

{{ $pages = $pages | lang.Merge .Site.RegularPages }}

剛剛取得到其他語言的 Page 之後,再使用 .Site.RegularPages 來取得該語言的所有 RegularPage,並且與當前語言的 RegularPage 結合,這樣就可以從其他語言中取得缺少的 Page。

再重新複習一下剛剛整段 code,我們假設當前的語言是 en,一樣有 tw, ja 其他兩種語言 Site。

{{ $pages := .Site.RegularPages }} // en Site 中所有的 RegularPages
{{ range .Site.Home.Translations }}

  // Loop 其他翻譯語言的 Page,此時 context 變成不同語言的 Page
  // 因此 .Site.RegularPages 是表示 `tw` `ja` 這兩個語言的 RegularPage
  // 雖然跟上面那行ㄧ樣,但實際的物件是完全不同的~

  {{ $pages = $pages | lang.Merge .Site.RegularPages }}

  // 將 Pages 合併,產生新的 Pages

{{ end }}

這樣聽起來 lang.Merge 好像很強大,如果有些內容來不及翻譯,就可以直接使用 default lang page 來替代。但實際上並沒有如此好用,使用 lang.Merge 的前提條件是該 page 會進入到 render 階段,我們才有辦法透過 template 來 merge 兩種語言的內容,如果那個 page 本身並不存在於 Site 當中,那他就不會 render,此時當然就無法使用 lang.Merge 功能。

舉個例子,如果我們有 entw 兩個語言的 Site,但是只有 about.md 英文版本的內容,缺少了 about.tw.md 文件,此時即使我們在 layout/about 加入 lang.Merge ㄧ樣沒用,因為缺少 about.tw.mdtw Site 就不會有 about page object,進而就不會進入 template render。

問題:share a content to different language sites

綜合以上的說明,可以發現使用 Hugo build 多語言的網站時,會遇到一個問題,就是一定要有 *.tw.md 這類語言頁面 file 的存在,才會產出該頁面,而 lang.Merge 並無法有效解決該類問題。在 Hugo issues 中也看到不少 user 建議要有 merge content with default language content,像是 Issues #5612,而 Hugo 開發者也認同此建議,只是他也點出做這功能的複雜度,像是: merge 要以哪個語言為主?如果使用 merge,那 Hugo 會產出大量不同語言但是其實內容一樣的 html,Google search 表示無奈。

因此雖然有這個 Proposal,但是目前被持續遞延到下一個版本。在沒有其他官方作法之前,網友有提供各種方式來暫時解決此問題,像是建立 symbolic link ln -s about.md about.tw.md 等。

結論

Hugo 的多語言網站建立還是有其限制,因此在使用 Hugo 建立商業官方網站之前,最好先評估一下是否可以接受這點,不然直接使用 javascript front-end framework 搭配 i18n,可能比處理這些流程還要好些。