10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2019-02-10

はじめに

これまで自前だったサイトジェネレーターからHugoに移行を始めています。

ついでに、Data Templatesの仕組みを利用してベタ打ちだった内容を、外部データから自動生成する事も目指しています。

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

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

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

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 に以下のような変更を加えています。

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 では、content/直下のディレクトリを言語毎に分けています。複数チームでメインの文書を作成するチームと翻訳するチームといった分担をする場合には、それぞれ干渉しないために、トップディレクトリを分けた方が適当に思えます。

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

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

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

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

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

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

$ hugo new site proj_root

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

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/...
    + ...

最初に利用した"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のように用途毎にファイルを配置しています。

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への追加設定

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

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セクションが次のようになっているとします。

[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 VariablesPage Variableのリファレンスを確認する必要があります。

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

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

公式discourseフォーラムへの投稿の中で、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}/のようにトップレベルで分けています。

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 形式で変数を記述していくとエラーになります。

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

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

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

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

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

基本的に単数・複数形を使い分ける文化圏の考え方で整備されているので、one変数とother変数を使い分ける形になっています。公式ページgo-i18n公式

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

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

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

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

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

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

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

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

例えば、Using linked translations in your template など、言語間を遷移するリンクの挿入する方法はまとめられています。この例はheaderにlinkタグを埋め込むものなので、コンテンツ内のnav部分にリンクを挿入するため次のようなコードを利用しています。

{{ 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メニューにする例なども掲載されています。

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

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

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

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ファイル等添付ファイルの置き場所について

【2026/01/14追記】 content/ディレクトリからstatic/にファイルをコピーするスクリプトを配置し、内容を変更しています。

PNG, JPEGなどの画像ファイルはcontent/ディレクトリに配置するとpublic/ディレクトリに適切にコピーされてきましたが、バージョンが変更されると挙動が繰り返し変更されてきました。

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

ファイル配置の例(1)
content/profile.en.adoc
content/profile.ja.adoc
content/profile.ref.ja.pdf
content/profile.ref.en.pdf
content/profile.map.pdf     ## profile.{en,ja}.adocの両方から参照される言語非依存の共通ファイル

セクションにならないページとして管理したコンテンツがある場合には、次のようなディレクトリ構造も採用しています。

ファイル配置の例(2)
content/profile/_index.en.adoc
content/profile/_index.ja.adoc
content/profile/ref.ja.pdf
content/profile/ref.en.pdf
content/profile/map.pdf
content/profile/subprofile.ja.adoc ## セクションではなくページとして管理したいコンテンツ
content/profile/subprofile.en.adoc ## (同上)

content/ディレクトリにリソースを配置する方法は、Page Resourcesとしてドキュメントが整備されています。

上記ドキュメントでは次のような設定をプロジェクト直下のhugo.tomlなどに行うことで、public/ディレクトリの両言語のディレクトリにファイルを配置すると記述されていますが、少なくとも最新のv0.154.5では無効で挙動に変化はありません。

確認した範囲ではv0.152.4v0.154.5の間で、public/ディレクトリに配置されるリソースファイルに違いがあります。

ファイル配置の例(2)とPage Resourcesの利用はディレクトリベースでのコンテンツの記述と管理に便利です。

また相対パスでのリソース指定も簡単で、間違いが入り込みにくいため利用したいのですが、バージョンアップの度に以前のpublic/ディレクトリの内容と比較する必要があり、気を使う部分でもありました。

v0.154.5にバージョンアップしたタイミングで問題が再現したため、content/ディレクトリからファイルをコピーするための簡単なスクリプトを作成して、hugoコマンドを実行する直前に実行するようにしてgitからstatic/{ja,en}/ディレクトリを管理対象から外しました。

layouts/ディレクトリのテンプレートから参照するサイト全体のリソース(コーポレートロゴなど)は、static/images/ディレクトリなどに配置し、引き続きgitで管理しています。

static/{ja,en}/ディレクトリを構成するsetup-static.rbスクリプト
#!/usr/bin/env ruby

require 'fileutils'

FILESUFFIXS = ['.pdf', '.png', 'yaml', '.jpeg', '.jpg', '.txt', '.conf']
LANGS = ['en', 'ja']

FILESUFFIXS.each do |suffix|
    Dir.glob('content/**/*' + suffix) do |filepath|
      puts format "Target file: %s", filepath
      LANGS.each do |lang|
        to_file = filepath.sub(/^content/, "static/#{lang}")
        target_dir = File.dirname(to_file)
        puts format "  Copy file: %s", to_file
        unless File.directory?(target_dir)
            puts "  Create target directory: #{target_dir}"
            FileUtils.mkdir_p(target_dir)
        end
        FileUtils.cp(filepath, to_file)
      end
    end
end

Cacheをうまく使ってコピー処理を減らす工夫や、例外への対応は不足していますが、とりあえず動く最低限のスクリプトはこのような内容になっています。

以上

10
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?