Hugoでのi18n+Dataファイル機能を使う時のユースケースまとめ

# はじめに

これまで自前だったサイトジェネレーターから[Hugo](http://gohugo.io/)に移行を始めています。

ついでに、[Data Templates](https://gohugo.io/templates/data-templates/)の仕組みを利用してベタ打ちだった内容を、外部データから自動生成する事も目指しています。

日英に対応する必要があり、hugoのi18n機能についてあまり情報がなかったので、Data Templatesの機能と合わせて、メモを残しておくことにしました。

# Hugo v0.9x以降を利用する際の考慮点

Hugoのバージョンアップ後、セキュリティモデルが変更されたため、asciidoctorコマンドを利用する場合、次のようなエラーになります。

```text:hugo実行時のエラー例
hugo v0.92.2+extended linux/amd64 BuildDate=unknown
Error: Error building site: "/home/user01/hugo/site01/content/_index.ja.adoc:1:1": access den
ied: "asciidoctor" is not whitelisted in policy "security.exec.allow"; the current security configuration is:
                              
[security]                           
  enableInlineShortcodes = false                         
  [security.exec]           
    allow = ['^dart-sass-embedded$', '^go$', '^npx$', '^postcss$']
    osEnv = ['(?i)^(PATH|PATHEXT|APPDATA|TMP|TEMP|TERM)$']
                            
  [security.funcs]          
    getenv = ['^HUGO_']     
             
  [security.http]                                 
    methods = ['(?i)GET|POST']                                                                                
    urls = ['.*']     
```

これに対応するため、config.toml に以下のような変更を加えています。

```toml:config.toml
[security]
  enableInlineShortcodes = true
  [security.exec]
    allow = [ "^asciidoctor$", "^sass$" ]
```

::note
config.tomlは最近の``hugo new site``コマンドでは``hugo.toml``になっているようです。
この例では"sass"も有効化していて、"assets/sass/"ディレクトリにsass/scssファイルを配置しています。
:::

# Hugo i18n機能の利用方針
content/ ディレクトリの構造として、イントラネットのサイトは、
```content/{en,ja}/``` のようにディレクトリ階層の中で言語を分離しています。

また別に作成しているサイトでは、```content/_index.{en,ja}.adoc```のように同一階層の中でファイルで言語を切り替える方法を採用しています。

これまで利用してきて、前者の content/{jp,en}/ のような構造は、無駄が多いと感じています。理由は、トップレベルでディレクトリを分けてしまった場合に、その下の構造を一致させるには一定の労力が必要になるからです。

大規模プロジェクトでHugoを利用している例として[github.com/kubernetes/website.git](https://github.com/Kubernetes/website.git) では、content/直下のディレクトリを言語毎に分けています。複数チームでメインの文書を作成するチームと翻訳するチームといった分担をする場合には、それぞれ干渉しないために、トップディレクトリを分けた方が適当に思えます。

しかし、一人ないしは単一のチームがコンテンツの更新を担当しているケースでは、ファイルのsuffixで、.{ja,en}.md と分ける後者のタイプが作業効率や構造の把握といった面からは便利だと思います。

Kubernetes/website.gitのconfig.toml上では、contents/{en,cn,...}/ のようにディレクトリを言語毎に分けていますが、``DefaultContentLanguage = "en"`` かつ ``defaultContentLanguageInSubdir = false``としていて、英語以外の言語を選択した場合に、/cn/のような言語毎のサブディレクトリに誘導されます。

``defaultContentLanguageInSubdir = true`` とすれば、en(英語)をデフォルトとして表示されますが、全ての言語が並列に配置されます。サイトを構成する際に、日本語版を正として、英語版は参考情報として正確さを担保したくないといった場合には、``DefaultContentLanguage = "ja"``、``defaultContentLanguageInSubdir = false`` とする方法もあるかもしれません。

いずれの場合も、static/ ディレクトリは、これら設定の影響を強く受けるので、共通で利用するfavicon.icoファイルなどの管理には適していますが、多言語のコンテンツから参照するような用途では使用しないようにした方が良いと思っています。

# 利用しているディレクトリ構造の概要

hugoコマンドで、*proj_root*以下にデフォルトのディレクトリ構造を作成します。

```bash
$ hugo new site proj_root
```

さらにディレクトリ、ファイルを配置し、以下のようなディレクトリ構造としています。

```text
proj_root/
    + i18n/en.toml
    + i18n/ja.toml
    + data/en/activity/20170401.01.toml
    + data/en/activity/...
    + data/en/menu/nav_header.yaml
    + data/en/menu/nav_main.yaml
    + data/en/menu/...
    + data/ja/activity/20170401.01.toml
    + data/ja/activity/...
    + data/ja/menu/nav_header.yaml
    + data/ja/menu/nav_main.yaml
    + data/ja/menu/...
    + content/en/activity/20170401.01.adoc
    + content/en/...
    + content/ja/...
    + layouts/...
    + ...
```

:::note warn
最初に利用した"nav-main.yaml"のようにハイフンを含むファイル名では問題が発生しました。

ファイル名を利用してデータにアクセスするため、```$data.menu.nav-main```のような指定をすると、'-'の1文字が原因でエラーとなります。
:::

*i18n/* 以下には{en,ja}.tomlの2つのファイルだけを配置していて、辞書的な使い方を想定しています。当初は、headerの文言やmavメニューの文言について、config.tomlの[languages.{en,ja}.params]の中に置いていましたが、現在では、data/以下にmenu/のようなディレクトリを作成し、nav_header.yaml, nav_main.yamlのように用途毎にファイルを配置しています。

:::note
Hugoのi18n機構は、独自にパッケージ化されていますが内容は``go-i18n/v2``です。

GNU gettextなどのシンプルな翻訳ライブラリと比較すると、複数形への対応はユニークな特徴ではないでしょうか。(他にはMozilla Fluentのように性別を意識するライブラリもあります。)

複数形に対応するためにラベルに対して``other``の他に、``few/many``,``one/two`` といった変数に値を割り当てることができます。

実践的な使い方としては``[days_by_num]``のようなセクションに``other = "{{ . }} days"``, ``one = "{{ . }} day"``のような引数を展開してラベルを指定する(``{{ i18n "days_by_num" 14 }}``)ことになるでしょう。
:::

# config.tomlへの追加設定

最小限の設定は、次のようになります。

```toml
baseURL = "http://localhost:1313/"
hasCJKLanguage = true
DefaultContentLanguage = "ja"
defaultContentLanguageInSubdir = true

[languages]
  [languages.ja]
    languageName = "日本語"
    languageCode = "ja"
    contentDir = "content/ja"
    weight = 1
    [languages.ja.params]
      key1 = val1
  [languages.en]
    languageName = "English"
    languageCode = "en"
    contentDir = "content/en"
    weight = 2
    [languages.en.params]
      key1 = val1
```

[languages.{ja,en}.params]に追加した項目は、処理している言語に応じて、.Site.Params以下に展開されます。

# config.tomlに設定する値と、.Siteなどへの展開について

config.toml上に設定した値の多くは、.Siteのprefixを通じてアクセスが可能です。
ただし、ルールが分かりにくいように感じています。

```googleAnalytics: UA-123-45```のように設定した値は、```{{.Site.GoogleAnalytics }}```のようにアクセスすることができます。ここで、config.tomlに記述した内容が、 ```{{ .Site.xxx }}``` のようにアクセスできるのだと理解すると、間違うかもしれません。この例でも先頭の小文字が大文字になっていて、実際はドキュメントを参照する必要があります。

例えば、Languageセクションが次のようになっているとします。

```toml
[languages]
  [languages.ja]
    languageName = "日本語(JP)"
    languageCode = "JA"
```

それぞれ次のような方法で参照が可能です。

* .Site.Language.LanguageName → "日本語(JP)" (変更可能)
* .Site.LanguageCode → "JA" (変更可能)

また、.Page(.)経由で、.Languageにアクセスができるので、LanguageNameは次のような方法でも記述できますが、LanguageCodeはそう動かないので、次のようにエラーとなります。

* .Language.LanguageName → "日本語(JP)
* .LanguageCode → エラー

現在処理中のロケールは次のように参照できますが、config.tomlに記述する```[languages.XX]```に対応する```XX```が格納される仕様のようで変更はできなさそうです。

* .Site.Language.Lang → "ja" 
* .Page.Lang == .Lang → "ja"

ロケールに関連する情報は、[Site Variables](https://gohugo.io/variables/site/)や[Page Variable](https://gohugo.io/variables/page/)のリファレンスを確認する必要があります。

# 公式サイトで配布されているテーマの利用について

[themes.gohugo.io](https://themes.gohugo.io/)で多くのテーマが提供されていますが、それぞれ前提とする変数などが違うため、テーマ毎にconfig.tomlファイルなどの内容を書き換える必要があります。

[公式discourseフォーラムへの投稿](https://discourse.gohugo.io/t/learning-how-to-switch-themes/5025)の中で、wordpressと比較して使いづらいといった意見があり、標準化が行なわれていない旨の記述も確認できます。

多くのテーマがbootstrap,w3css,font-awesomeなどを利用しつつ、独自に文書構造をデザインしています。
そのため、コピーライト表示の有無といった表示の制御では、config.tomlに共通の設定が確認できるテーマは多いものの、細かい部分ではテーマ毎にconfig.tomlに書かなければいけない内容、セクション構造が異なるため、互換性はほぼありません。

テーマの切り替えを目指すのであれば、最終的には自分で違いを吸収するための努力を払うことが必要になります。
多言語化やPagination機能を利用するメリットは大きいですが、自分のサイトを「気分によっていろいろなテーマに切り替えたい」と思う人にとってはあまり向かないツールだと思います。

# ユースケース (UC)

個別のケース毎に対策をまとめていきます。

## 【UC】言語毎に読み込むDataファイルを切り替える

data/{ja,en}ディレクトリ以下にフィアルを配置している場合、次のようにindex関数を利用して、ターゲットとなる言語のデータ一式を取得します。ポイントは、変数を利用する際に、```.Site.Data.{{Site.LanguageCode}}.activity```のような変数参照を間に挟む書式を受け付けてくれないため、分割してアクセスする必要がある点です。

一度に取得するデータを小さく保つため、*data/menu/{en,ja}/data.toml*のようなアクセス方法も可能性としてはあると思いますが、私は```data/{en,ja}/```のようにトップレベルで分けています。

```md:layout/以下のテンプレートでdata/{ja,en}/menu/nav_main.yamlを参照する例
{{ $data := index .Site.Data .Site.LanguageCode }}
{{ range $data.menu.nav_main }}
...
{{ end }}
```

## 【UC】i18n/ディレクトリに配置した言語別ファイルの利用

i18n/ja.toml,i18n/en.tomlファイルに、直接 ``` key = value ``` 形式で変数を記述していくとエラーになります。

```bash
$ hugo
Building sites … panic: interface conversion: interface {} is string, not map[string]interface {}
```

i18nに配置するファイルでは必ずセクション([])を切って、その中のother変数(もしくはone)に内容を記述します。

```toml
[site_title]
other = "Yasuhiro ABE's Web"
```

そして、layout/_default/baseof.html の中などのテンプレートで参照します。

```html
...
  <title>{{ i18n "site_title" . }}</title>
...
```

基本的に単数・複数形を使い分ける文化圏の考え方で整備されているので、one変数とother変数を使い分ける形になっています。[公式ページ](https://gohugo.io/content-management/multilingual/#translation-of-strings)、[go-i18n公式](https://github.com/nicksnyder/go-i18n)

実際には、ほぼother変数に固定されているので、直感とは少し違ったイメージになっています。

## 【UC】config.toml に言語毎に文字列を設定する

i18n/ja.toml以外に、config.toml の中に言語毎の変数を設定する事が可能です。

```toml:config.toml
[languages]
  [languages.ja]
    [languages.ja.params]
      author = "作者"
  [languages.en]
    [languages.en.params]
      author = "Author"
```

これを layout/partials/header.html などから参照するには次のようにします。

```html
<dl>
  <dt>{{ .Site.Params.author }}</dt>
...
```

しばらく使ってみた結果、config.tomlにはサイト全体についての情報を集約するべきだと思っています。
単純な単語の訳であれば、i18n/にあるファイルを充実させるべきだと思います。

## 【UC】日or英版のページへのリンクを挿入する

例えば、[Using linked translations in your template](https://regisphilibert.com/blog/2018/08/hugo-multilingual-part-1-managing-content-translation/) など、言語間を遷移するリンクの挿入する方法はまとめられています。この例はheaderにlinkタグを埋め込むものなので、コンテンツ内のnav部分にリンクを挿入するため次のようなコードを利用しています。

```html
{{ if .IsTranslated }}
  {{ range .Translations }}
    <div id="lang-selector">[ <a href="{{ .Permalink }}"
         alt="Switch to {{.Language.LanguageName}}">{{ .Language.LanguageName }}</a> ]</div>
  {{ end }}
{{ end }}
```

HugoのDiscourseをみているとkubernetesのように[Pulldownメニューにする例](https://discourse.gohugo.io/t/language-switcher-in-menu/11570/4)なども掲載されています。

## 【UC】リストから条件に合うデータだけを表示する

range,withなどでループに入っている時に、全部を表示せずに特定の文字列を含むデータだけにアクセスしたい。

様々な実装が考えられますが、front matterに適当なキーワードを```DataKey = "key"```のように記述しておくことで、そのページで読み込むデータのタイプを指定することができます。

```text:layout/以下のテンプレート
  {{ if in .date $.DataKey }}
  <h3>{{ .date }}</h3>
  <p>{{ .name }}</p>
  {{ end }}
```

この例では```if in```を使うことで完全一致ではなく、部分一致を利用しています。
他には、```if isset <list> <arg>```や```if eq <arg1> <arg2>```といった表現があり、eqは数値でも文字列でも使えます。

## 【UC】ローカルに配置するPDFファイル等添付ファイルの置き場所について

:::note warn
【2025/02/18追記】 このセクションの内容を大幅に書き換えました。
:::

PNG, JPEGなどの画像ファイルはcontent/ディレクトリに配置するとpublic/ディレクトリに適切にコピーされます。

現状では次の例(1)を基本として、セクションの管理上必要な場合には例(2)を使用しています。

```bash:ファイル配置の例(1)
contents/profile.en.adoc
contents/profile.ja.adoc
contents/profile.ref.ja.pdf
contents/profile.ref.en.pdf
contents/profile.map.pdf     ## profile.{en,ja}.adocの両方から参照されるファイル
```

```bash:ファイル配置の例(2)
contents/profile/_index.en.adoc
contents/profile/_index.ja.adoc
contents/profile/ref.ja.pdf  ## 日本語版
contents/profile/ref.en.pdf  ## 英語版
contents/profile/map.pdf     ## profile.{en,ja}.adocの両方から参照されるファイル
contents/profile/subprofile.ja.adoc ## セクションではなくページとして管理したいコンテンツ
contents/profile/subprofile.en.adoc ## (同上)
```
contents/ ディレクトリの中に、PDFやPNGファイルを配置することは良いアイデアだと思います。

しかし、HugoのバージョンによってPDFやPNGファイルが、適切にsuffixを付けていても.../ja/や.../en/の片側にしかコピーされない時がありました。

現在では全てのファイルがコピーされるはずですが、多言語対応する場合にはHugoのバージョンによって挙動に違いがないか確認することをお勧めします。

以上