投稿

PolyglotでJekyllブログの多言語対応を実現する方法 (2) - 言語選択ボタンの実装 & レイアウト言語の現地化

'jekyll-theme-chirpy'ベースのJekyllブログにPolyglotプラグインを適用して多言語対応を実装した過程を紹介する。この投稿は該当シリーズの2番目の記事として、言語選択ボタンの実装とレイアウト言語の現地化部分を扱う。

PolyglotでJekyllブログの多言語対応を実現する方法 (2) - 言語選択ボタンの実装 & レイアウト言語の現地化

概要

12024年7月初旬、Jekyll基盤でGitHub Pagesを通じてホスティング中の本ブログにPolyglotプラグインを適用して多言語対応実装を追加した。 このシリーズはChirpyテーマにPolyglotプラグインを適用する過程で発生したバグとその解決過程、そしてSEOを考慮したhtmlヘッダーとsitemap.xmlの作成法を共有する。 シリーズは3つの記事で構成されており、読んでいるこの記事は該当シリーズの2番目の記事である。

元々は全2編で構成していたが、その後数回にわたって内容を補強したことにより分量が大幅に増加したため、3編に改編した。

要求条件

  • ビルドした結果物(ウェブページ)を言語別パス(例:/posts/ko//posts/ja/)で区分して提供できなければならない。
  • 多言語対応に追加的に要する時間と労力を可能な限り最小化するため、作成した原本マークダウンファイルのYAML front matterに’lang’及び’permalink’タグを一々指定しなくても、ビルド時に該当ファイルが位置するローカルパス(例:/_posts/ko//_posts/ja/)に応じて自動的に言語を認識できなければならない。
  • サイト内各ページのヘッダー部分は適切なContent-Languageメタタグとhreflang代替タグ、canonical linkを含んで多言語検索のためのGoogle SEOガイドラインを満たさなければならない。
  • サイト内で各言語バージョン別ページリンクを漏れなくsitemap.xmlで提供できなければならず、sitemap.xml自体は重複なくルートパスに一つだけ存在しなければならない。
  • Chirpyテーマで提供するすべての機能は各言語ページで正常動作しなければならず、そうでなければ正常動作するよう修正しなければならない。
    • ‘Recently Updated’、’Trending Tags’機能の正常動作
    • GitHub Actionsを利用したビルド過程でエラーが発生しないこと
    • ブログ右上の投稿検索機能の正常動作

始める前に

この記事は1編から続く記事なので、まだ読んでいない場合は先に前の記事から読むことを推奨する。

サイドバーに言語選択ボタンを追加

(12025.02.05. アップデート)言語選択ボタンをドロップダウンリスト形式に改善した。

_includes/lang-selector.htmlファイルを生成し、次のように内容を入力した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<link rel="stylesheet" href="{{ '/assets/css/lang-selector.css' | relative_url }}">

<div class="lang-dropdown">
    <select class="lang-select" onchange="changeLang(this.value)" aria-label="Select Language">
    {%- for lang in site.languages -%}
        <option value="{% if lang == site.default_lang %}{{ page.url }}{% else %}/{{ lang }}{{ page.url }}{% endif %}"
                {% if lang == site.active_lang %}selected{% endif %}>
            {% case lang %}
            {% when 'ko' %}🇰🇷 한국어
            {% when 'en' %}🇺🇸 English
            {% when 'ja' %}🇯🇵 日本語
            {% when 'zh-TW' %}🇹🇼 正體中文
            {% when 'es' %}🇪🇸 Español
            {% when 'pt-BR' %}🇧🇷 Português
            {% when 'fr' %}🇫🇷 Français
            {% when 'de' %}🇩🇪 Deutsch
            {% else %}{{ lang }}
            {% endcase %}
        </option>
    {%- endfor -%}
    </select>
</div>

<script>
function changeLang(url) {
    window.location.href = url;
}
</script>

またassets/css/lang-selector.cssファイルを生成し、次のように内容を入力した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
 * 言語選択器スタイル
 * 
 * サイドバーに位置する言語選択ドロップダウンのスタイルを定義します。
 * テーマのダークモードをサポートし、モバイル環境でも最適化されています。
 */

/* 言語選択器コンテナ */
.lang-selector-wrapper {
    padding: 0.35rem;
    margin: 0.15rem 0;
    text-align: center;
}

/* ドロップダウンコンテナ */
.lang-dropdown {
    position: relative;
    display: inline-block;
    width: auto;
    min-width: 120px;
    max-width: 80%;
}

/* 選択入力要素 */
.lang-select {
    /* 基本スタイル */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    width: 100%;
    padding: 0.5rem 2rem 0.5rem 1rem;
    
    /* フォント及び色彩 */
    font-family: Lato, "Pretendard JP Variable", "Pretendard Variable", sans-serif;
    font-size: 0.95rem;
    color: var(--sidebar-muted);
    background-color: var(--sidebar-bg);
    
    /* 形状及び相互作用 */
    border-radius: var(--bs-border-radius, 0.375rem);
    cursor: pointer;
    transition: all 0.2s ease;
    
    /* 矢印アイコン追加 */
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 1rem;
}

/* 国旗絵文字スタイル */
.lang-select option {
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
    padding: 0.35rem;
    font-size: 1rem;
}

.lang-flag {
    display: inline-block;
    margin-right: 0.5rem;
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
}

/* ホバー状態 */
.lang-select:hover {
    color: var(--sidebar-active);
    background-color: var(--sidebar-hover);
}

/* フォーカス状態 */
.lang-select:focus {
    outline: 2px solid var(--sidebar-active);
    outline-offset: 2px;
    color: var(--sidebar-active);
}

/* Firefoxブラウザ対応 */
.lang-select:-moz-focusring {
    color: transparent;
    text-shadow: 0 0 0 var(--sidebar-muted);
}

/* IEブラウザ対応 */
.lang-select::-ms-expand {
    display: none;
}

/* ダークモード対応 */
[data-mode="dark"] .lang-select {
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
}

/* モバイル環境最適化 */
@media (max-width: 768px) {
    .lang-select {
        padding: 0.75rem 2rem 0.75rem 1rem;  /* より大きなタッチ領域 */
    }
    
    .lang-dropdown {
        min-width: 140px;  /* モバイルでより広い選択領域 */
    }
}

その後、ChirpyテーマのChirpyテーマの_includes/sidebar.html中のsidebar-bottomクラス直前に次のようにlang-selector-wrapperクラス3行を追加して、先ほど作成した_includes/lang-selector.htmlの内容をJekyllがページビルド時に読み込むようにした。

1
2
3
4
5
6
7
  (前略)...
  <div class="lang-selector-wrapper w-100">
    {%- include lang-selector.html -%}
  </div>

  <div class="sidebar-bottom d-flex flex-wrap align-items-center w-100">
    ...(後略)

(12025.07.31. 機能追加)レイアウト言語の現地化

既存にはページタイトルと内容など本文コンテンツにのみ言語現地化を適用し、左側サイドバーのタブ名やサイト上下端及び右側パネルなどのレイアウト言語はサイト基本値である英語に固定していた。個人的にはその程度でも十分だったため、追加で作業する必要性をあまり感じなかったが、最近上述したOpen Graphメタデータ属性及び標準URL(canonical URL)パッチを作業する過程で、レイアウト言語現地化が少しの修正だけで非常に簡単に可能であることを発見した。大規模で面倒なコード修正作業が必要なら分からないが、10分もかからない簡単な作業だったので、ついでに追加適用した。

ロケールの追加

サイト内各ページに対して複数言語バージョンを同時に提供し、ユーザー選択に応じてバージョン間で切り替える機能がないだけで、Chirpyテーマがサポートする言語範囲自体は元々もかなり広い方である。したがってChirpyテーマが提供するロケールファイルの中から必要なものを選択的にダウンロードして追加し、必要な場合はファイル名だけ適切に修正すればよい。ロケールファイル名は先ほど設定構成段階で_config.ymlファイル内に定義したlanguagesリスト内項目と一致しなければならない。

実際すぐ後でも言及するが、_dataディレクトリのファイルは直接追加しなくてもjekyll-theme-chirpy gemを通じて基本提供される。

ただし私の場合には次のような理由でChirpyテーマが提供するロケールをそのまま使用するのが困難で、別途いくつかの修正が必要だった。

  • Chirpyテーマが基本提供するロケールファイルの名前形式がko-KRja-JPのように地域コードを含んでおり、今このサイトに使用中の形式(kojaなど)と一致しない
  • ライセンス案内文句を基本値であるCC BY 4.0ではなく、このブログのCC BY-NC 4.0に合わせて修正が必要
  • 韓国語日本語ロケールは韓国人である私が見るに少し不自然だったり、今このブログには合わないため個人的に修正した部分が存在する
  • 下に記述したように色々理由があって西暦紀元をあまり好まず、今このブログにだけは日付表記形式として人類紀元(Holocene calendar)を採択しているため、ロケールをそれに合わせて修正する必要があった
    • 根本的に特定宗教の宗教的色彩が強く西欧圏偏向的である
      • イエスが偉大な聖人だという点は否定しないし、該当宗教の立場も尊重するため、仏教の仏滅紀元のように西暦紀元もその宗教内部的にのみ使うというなら全く問題ないが、そうではないから問題を提起するのである。孔子、釈迦、ソクラテスなどなど、その他にも多くの聖人がいたのに、非宗教人や他の宗教を信じる人々、そしてヨーロッパ以外の他文化圏の立場で全世界が使う紀年法の元年が굳이イエスの誕生年度である理由は何か?
      • そしてその「元年」が本当にイエス誕生年度なのかと言えば、実際はそうでもなく、それより数年前に誕生したというのが定説である
    • 「0」の概念が登場する前に考案された紀年法なので、紀元前1年(-1)の次の年がすぐに西暦1年(1)という点で年度計算が直感的でない
    • 人類の新石器時代及び農耕社会進入以後、イエス誕生前までの10000年、文字発明以後のみ考慮しても3000-4000年に達する歴史を「紀元前」でまとめるが、このため世界史、特に古代史において認知的な歪曲を誘発する

そのためここでは_data/localesディレクトリにロケールファイルを直接追加後、適当に修正して適用したのである。
したがって該当事項がなく、Chirpyテーマが基本提供するロケールを修正なしでそのまま適用するなら、この段階は飛ばしても良い。

Polyglotとの統合

今度は次の2つのファイルだけ少しずつ修正すれば、Polyglotと滑らかに統合できる。

最初にリポジトリを生成する時、テーマリポジトリを直接フォークせずにChirpy Starterを使用した場合なら、該当するファイルが本人のサイトのリポジトリにはない可能性もある。直接追加しなくてもjekyll-theme-chirpy gemを通じて基本提供されるファイルだからだが、その場合にはChirpyテーマリポジトリから該当するファイル原本を先にダウンロードして本人のリポジトリ内同一位置に置いた後作業すればよい。Jekyllがサイトをビルドする時、リポジトリ内に同一名のファイルが既にある場合、外部gem(jekyll-theme-chirpy)で提供するファイルより優先的に適用する。

‘_includes/lang.html’

下記のように_includes/lang.htmlファイル中間にコード2行を追加して、ページのYAML front matterに別途lang変数を明示して指定しなかった場合、_config.ymlに定義されたサイト基本言語(site.lang)や英語('en')よりPolyglotのsite.active_lang変数を優先的に認識するようにする。該当ファイルはChirpyテーマを適用したサイト内のすべてのページ(_layouts/default.html)でビルド時lang変数宣言のため共通的に呼び出すファイルで、ここで宣言するlang変数を利用してレイアウト言語現地化を実行する。

1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -1,10 +1,12 @@
 {% comment %}
   Detect appearance language and return it through variable "lang"
 {% endcomment %}
 {% if site.data.locales[page.lang] %}
   {% assign lang = page.lang %}
+{% elsif site.data.locales[site.active_lang] %}
+  {% assign lang = site.active_lang %}
 {% elsif site.data.locales[site.lang] %}
   {% assign lang = site.lang %}
 {% else %}
   {% assign lang = 'en' %}
 {% endif %}

lang変数宣言時の優先順位:

  • 修正前:
    1. page.lang(個別ページのYAML front matter内に定義された場合)
    2. site.lang_config.ymlに定義された場合)
    3. 'en'
  • 修正後:
    1. page.lang(個別ページのYAML front matter内に定義された場合)
    2. site.active_lang(Polyglotを適用中の場合)
    3. site.lang_config.ymlに定義された場合)
    4. 'en'

‘_layouts/default.html’

同様に_layouts/default.htmlファイルの内容を修正して、HTML文書最上位要素である<html>タグにlang属性を正しく指定するようにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@@ -1,19 +1,19 @@
 ---
 layout: compress
 ---
 
 <!doctype html>
 
 {% include origin-type.html %}
 
 {% include lang.html %}
 
 {% if site.theme_mode %}
   {% capture prefer_mode %}data-mode="{{ site.theme_mode }}"{% endcapture %}
 {% endif %}
 
 <!-- `site.alt_lang` can specify a language different from the UI -->
-<html lang="{{ page.lang | default: site.alt_lang | default: site.lang }}" {{ prefer_mode }}>
+<html lang="{{ page.lang | default: site.active_lang | default: site.alt_lang | default: site.lang }}" {{ prefer_mode }}>
   {% include head.html %}

<html>タグlang属性指定時の優先順位:

  • 修正前:
    1. page.lang(個別ページのYAML front matter内に定義された場合)
    2. site.alt_lang_config.ymlに定義された場合)
    3. site.lang_config.ymlに定義された場合)
    4. unknown(空文字列、lang=""
  • 修正後:
    1. page.lang(個別ページのYAML front matter内に定義された場合)
    2. site.active_lang(Polyglotを適用中の場合)
    3. site.alt_lang_config.ymlに定義された場合)
    4. site.lang_config.ymlに定義された場合)
    5. unknown(空文字列、lang=""

ウェブページ言語(lang属性)を指定せずにunknownにしておくことは推奨されず、可能な限り適切な値で指定しておくべきである。見ての通り_config.yml内のlang属性値をfallbackとして使用するため、Polyglotを使用するかしないかに関わらず、この値は必ず適切に定義しておくのが良く、正常な場合なら通常は既に定義されているはずである。この記事で扱うようにPolyglotまたはそれと類似したi18nプラグインを適用した場合なら、site.default_langと同じ値で指定するのが無難だろう。

Further Reading

Part 3に続く

この投稿は投稿者によって CC BY-NC 4.0 の下でライセンスされています。