紀錄用 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 的基本物件內容:
Multi-language Sites
有了前面的概念之後,就可以知道 multi-language site 是怎麼產出的了,它其實就是 render 成不同語言的獨立頁面,然後透過 path 來分類出當前的頁面是哪個語言,其目錄結構如下:
而就是因為這樣的目錄結構,我們才可以簡單地在 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 有關,例如:
1languages:
2 en:
3 languageName: "English"
4 weight: 1
5 tw:
6 languageName: "繁體中文"
7 weight: 2
對應的 .Site.Language.Lang
就是 tw
;而 .Site.Language.LanguageName
就是 繁體中文
。我會建議這個命名最好與其他 resources 設定一樣的命名規則,舉例來說,如果語言要顯示出對應的 icon,而 icon 的class name 是 tw
,那麼這個名稱也要設定成 tw
,如此一來在寫 template 的時候,就可以很方便的寫成:
1<p>
2 <span class="flag-icon-{{ .Site.Language.Lang }}"></span>
3 .Site.Language.LanguageName
4</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
,官方文件在介紹時會提及一個範例:
1{{ if .IsTranslated }}
2<h4>{{ i18n "translations" }}</h4>
3<ul>
4 {{ range .Translations }}
5 <li>
6 <a href="{{ .Permalink }}">{{ .Lang }}: {{ .Title }}{{ if .IsPage }} ({{ i18n "wordCount" . }}){{ end }}</a>
7 </li>
8 {{ end }}
9</ul>
10{{ 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,因此產出結果會變成:
1<ul>
2 <li>
3 <a href="www.example.com/tw/about">tw: 繁中版本字數20</a>
4 <a href="www.example.com/ja/about">ja: 日文字數30</a>
5 </li>
6</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
功能來呈現英文內容。
官網範例如下:
1{{ $pages := .Site.RegularPages }}
2{{ range .Site.Home.Translations }}
3 {{ $pages = $pages | lang.Merge .Site.RegularPages }}
4{{ 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
這類的變數出現。
接著,
1{{ range .Site.Home.Translations }}
如同上面提到, .Translations
是列出所有翻譯過的 Page,但是為什麼要特別指定 .Site.Home
呢?主要原因是 .Site.Home
意指網站的 home page ,而 home page 是每個翻譯版本都需要的 Page,所以透過 home page,才能真正的取得所有翻譯版本的 Page。如果只用 .Translations
的話,有可能當前 Page 並沒有其他翻譯版本,那就會回傳空 array。
1{{ $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
功能。
舉個例子,如果我們有 en
和 tw
兩個語言的 Site,但是只有 about.md
英文版本的內容,缺少了 about.tw.md
文件,此時即使我們在 layout/about
加入 lang.Merge
ㄧ樣沒用,因為缺少 about.tw.md
,tw
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,可能比處理這些流程還要好些。