Thymeleaf を使用した JBake 静的サイトの国際化
公開日: 20 October 2025
はじめに
静的サイトの国際化(i18n)は一見複雑に見えるかもしれませんが、JBakeとThymeleafを組み合わせることで、多言語サイトを作成するための洗練されたソリューションが提供されます。この記事では、私のブログでi18nをどのように実装したかを紹介し、テンプレートと複数言語での記事管理の両方をカバーします。
ユースケース図 (Use Case)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "著者" as author
actor "リーダー FR" as reader_fr
actor "リーダー EN" as reader_en
rectangle "JBake多言語サイト" {
usecase "FR記事を書く" as UC1
usecase "記事をENで書く" as UC2
usecase "i18nメッセージを設定する" as UC3
usecase "サイト生成" as UC4
usecase "FR記事を読む" as UC5
usecase "記事を読む EN" as UC6
usecase "言語を変更" as UC7
usecase "利用可能な翻訳を見る" as UC8
}
author --> UC1
author --> UC2
author --> UC3
author --> UC4
reader_fr --> UC5
reader_fr --> UC7
reader_fr --> UC8
reader_en --> UC6
reader_en --> UC7
reader_en --> UC8
UC1 ..> UC4 : <<include>>
UC2 ..> UC4 : <<include>>
UC3 ..> UC4 : <<include>>
UC5 ..> UC8 : <<extend>>
UC6 ..> UC8 : <<extend>>
@enduml
国際化のアーキテクチャ
私たちのアプローチは2つの柱に基づいています:
-
テンプレートのi18n: インターフェース要素用のThymeleafメッセージファイルの使用
-
コンテンツの i18n言語ごとに記事を整理するための専用フォルダ構造
構造図(ファイル構成)
@startuml
@startsalt
{
{T
+ projet-jbake
++ content
+++ blog
++++ 2024
+++++ fr
++++++ article1.adoc
++++++ article2.adoc
+++++ en
++++++ article1.adoc
++++++ article2.adoc
++++ 2025
+++++ fr
++++++ guide-i18n.adoc
+++++ en
++++++ i18n-guide.adoc
++ templates
+++ post.html
+++ index.html
+++ index_en.html
+++ messages.properties
+++ messages_fr.properties
+++ messages_en.properties
++ assets
+++ css
+++ js
+++ img
++ jbake.properties
++ output
+++ fr
++++ blog
+++++ 2025
++++++ guide-i18n.html
+++ en
++++ blog
+++++ 2025
++++++ i18n-guide.html
}
}
@endsalt
@enduml
なぜこのアプローチ?
この分離により、以下のことが可能になります :
-
言語に関わらず、インターフェースの一貫性を維持する
-
コンテンツと記事の翻訳を独立して管理する
-
新しい言語の追加を大規模なリファクタリングなしで容易にする
-
特定の言語でのみ利用可能な記事を許可する
コンポーネント図
@startuml
skinparam componentStyle rectangle
package "出典" {
folder "content/blog/2025/" {
folder "fr/" as content_fr {
[article1.adoc]
[article2.adoc]
}
folder "en/" as content_en {
[article1.adoc] as article1_en
[article2.adoc] as article2_en
}
}
folder "テンプレート/" {
[post.html]
[index.html]
[index_en.html]
}
folder "メッセージ/" {
[messages_fr.properties]
[messages_en.properties]
}
[jbake.properties]
}
package "JBake エンジン" {
[Parser AsciiDoc] as parser
[Template Engine\nThymeleaf] as thymeleaf
[Generator] as generator
[I18n Resolver] as i18n
}
package "生成されたサイト" {
folder "fr/" {
folder "ブログ/2025/" as blog_fr {
[article1.html]
[article2.html]
}
}
folder "en/" {
folder "ブログ/2025/" as blog_en {
[article1.html] as article1_html_en
[article2.html] as article2_html_en
}
}
}
content_fr --> parser
content_en --> parser
parser --> generator
templates --> thymeleaf
messages --> i18n
i18n --> thymeleaf
thymeleaf --> generator
jbake.properties --> generator
generator --> blog_fr
generator --> blog_en
@enduml
Thymeleafを使用したテンプレートの国際化
メッセージファイルの構造
最初の手順は、サポートされている各言語用のプロパティファイルを作成することです:
src/jbake/templates/
├── messages.properties # Fallback par défaut
├── messages_fr.properties # Français
├── messages_en.properties # Anglais
└── messages_de.properties # Allemand
メッセージファイルの内容
ファイルの例です`messages_fr.properties`:
# Navigation
nav.home=Accueil
nav.blog=Blog
nav.about=À propos
nav.contact=Contact
# Articles
article.readmore=Lire la suite
article.published=Publié le
article.tags=Étiquettes
article.also.available=Également disponible en
# Interface
site.title=Mon Blog Technique
site.description=Partage de connaissances et expériences
footer.copyright=© 2025 Tous droits réservés
search.placeholder=Rechercher un article...
そしてその英語訳`messages_en.properties`:
# Navigation
nav.home=Home
nav.blog=Blog
nav.about=About
nav.contact=Contact
# Articles
article.readmore=Read more
article.published=Published on
article.tags=Tags
article.also.available=Also available in
# Interface
site.title=My Tech Blog
site.description=Sharing knowledge and experiences
footer.copyright=© 2025 All rights reserved
search.placeholder=Search articles...
テンプレートでの使用
Thymeleafのテンプレートでは、構文を使ってください`#{}`メッセージにアクセスする:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{site.title}">Mon Blog</title>
<meta name="description" th:content="#{site.description}" />
</head>
<body>
<nav>
<a th:href="@{/}" th:text="#{nav.home}">Accueil</a>
<a th:href="@{/blog/}" th:text="#{nav.blog}">Blog</a>
<a th:href="@{/about.html}" th:text="#{nav.about}">À propos</a>
</nav>
<footer>
<p th:text="#{footer.copyright}">Copyright</p>
</footer>
</body>
</html>
シーケンス図 - i18nの解決
@startuml
actor Auteur
participant JBake
participant "AsciiDoc
パーサー" as parser
participant "Thymeleaf\nエンジン" as thymeleaf
participant "I18n
リゾルバー" as i18n
database "messages_*.properties" as messages
Auteur -> JBake: jbake -b
activate JBake
JBake -> parser: Lire article.fr.adoc
activate parser
parser --> JBake: Contenu + métadonnées\n(lang=fr, article-id=xyz)
deactivate parser
JBake -> parser: Lire article.en.adoc
activate parser
parser --> JBake: Contenu + métadonnées\n(lang=en, article-id=xyz)
deactivate parser
JBake -> thymeleaf: Générer page FR
activate thymeleaf
thymeleaf -> i18n: Résoudre #{nav.home}
activate i18n
i18n -> messages: Lire messages_fr.properties
messages --> i18n: "ホーム"
i18n --> thymeleaf: "ホーム"
deactivate i18n
thymeleaf -> JBake: Rechercher traductions\n(article-id=xyz, lang!=fr)
JBake --> thymeleaf: article.en.html trouvé
thymeleaf --> JBake: /fr/blog/article.html
deactivate thymeleaf
JBake -> thymeleaf: Générer page EN
activate thymeleaf
thymeleaf -> i18n: Résoudre #{nav.home}
activate i18n
i18n -> messages: Lire messages_en.properties
messages --> i18n: "家"
i18n --> thymeleaf: "家"
deactivate i18n
thymeleaf -> JBake: Rechercher traductions\n(article-id=xyz, lang!=en)
JBake --> thymeleaf: article.fr.html trouvé
thymeleaf --> JBake: /en/blog/article.html
deactivate thymeleaf
JBake --> Auteur: Site généré
deactivate JBake
@enduml
JBakeにおけるロケールの設定
あなたのファイルで`jbake.properties`, デフォルトロケールを設定してください :
# Locale par défaut
thymeleaf.locale=fr
# Encodage
template.encoding=UTF-8
記事の国際化:フォルダーごとの整理
フローチャート (Flow) - 生成
@startuml
start
:Lire jbake.properties;
:Charger configuration i18n;
partition "各AsciiDocファイルごとに" {
:Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
:Parser contenu AsciiDoc;
:Stocker en mémoire avec langue;
}
partition "各テンプレートに対して" {
:Charger template Thymeleaf;
:Identifier langue cible;
:Charger messages_{lang}.properties;
partition "コンテンツフィルタリング" {
:Filtrer articles par langue;
:Grouper traductions\npar article-id;
}
:Appliquer template avec i18n;
:Générer HTML dans /{lang}/;
}
:Copier assets statiques;
:Générer sitemap multilingue;
:Générer flux RSS par langue;
stop
@enduml
フォルダ構造
ファイル名にサフィックスを使用する代わりに、フォルダによる整理を選択しました:
content/blog/
├── 2024/
│ ├── fr/
│ │ ├── introduction-jbake.adoc
│ │ ├── guide-thymeleaf.adoc
│ │ └── astuces-asciidoc.adoc
│ └── en/
│ ├── introduction-jbake.adoc
│ ├── thymeleaf-guide.adoc
│ └── asciidoc-tips.adoc
└── 2025/
├── fr/
│ └── internationalisation-jbake.adoc
└── en/
└── jbake-internationalization.adoc
このアプローチの利点
この構造にはいくつかの利点があります:
-
明確な分離各言語には独自のスペースがあります
-
�柔軟な命名: ファイルは言語によって異なる名前になる可能性があります
-
スケーラビリティ新しい言語を追加するのが簡単
-
自然な組織: JBakeの時間的論理に従う
記事のメタデータ
各記事は翻訳間のリンクを可能にするためにメタデータを含まなければなりません。以下に例を示します:
フランス語版(2025/fr/internationalisation-jbake.adoc) :
= Internationalisation d'un site statique JBake
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: fr
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide pour mettre en place l'i18n avec JBake
英語版(2025/en/jbake-internationalization.adoc) :
= Internationalizing a JBake Static Site
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: en
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide to implement i18n with JBake
属性`:jbake-article-id:`は重要です:それは、同じ記事のさまざまな訳を結びつけることを可能にします。
URLの設定
中に`jbake.properties`, URLパターンを言語を含むように設定してください :
# Pattern d'URL avec langue
post.permalink.pattern=:lang/blog/:year/:name.html
# Langue par défaut
site.default.lang=fr
これにより、次のような URL が生成されます:
-
/fr/blog/2025/internationalisation-jbake.html -
/en/blog/2025/jbake-internationalization.html
多言語表示用テンプレート
言語セレクタ付き記事テンプレート
テンプレートを作成してください`post.html`利用可能な翻訳を表示します :
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${content.title}">Article</title>
</head>
<body>
<article>
<header>
<h1 th:text="${content.title}">Titre</h1>
<div class="article-meta">
<time th:text="${#dates.format(content.date, 'dd MMMM yyyy')}"
th:attr="datetime=${#dates.format(content.date, 'yyyy-MM-dd')}">
Date
</time>
<!-- Sélecteur de traductions -->
<div class="translations" th:if="${content['article-id']}">
<span th:text="#{article.also.available}">Aussi disponible en :</span>
<ul class="language-list">
<li th:each="post : ${published_posts}"
th:if="${post['article-id'] == content['article-id'] and post.lang != content.lang}">
<a th:href="${post.uri}"
th:text="${post.lang.toUpperCase()}">
LANG
</a>
</li>
</ul>
</div>
</div>
</header>
<div class="content" th:utext="${content.body}">
Contenu de l'article
</div>
<footer class="article-footer">
<div class="tags" th:if="${content.tags}">
<span th:text="#{article.tags}">Étiquettes :</span>
<span th:each="tag : ${content.tags}">
<a th:href="@{/tags/{tag}.html(tag=${tag})}"
th:text="${tag}">tag</a>
</span>
</div>
</footer>
</article>
</body>
</html>
言語でフィルタリングされたインデックス
各言語向けのインデックステンプレートを作成してください:
index.html(フランス語インデックス) :
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{site.title}">Mon Blog</title>
</head>
<body>
<main>
<h1 th:text="#{nav.blog}">Blog</h1>
<div class="articles-list">
<article th:each="post : ${published_posts}"
th:if="${post.lang == 'fr'}">
<h2>
<a th:href="${post.uri}" th:text="${post.title}">Titre</a>
</h2>
<time th:text="${#dates.format(post.date, 'dd MMMM yyyy')}">
Date
</time>
<p th:text="${post.description}">Description</p>
<a th:href="${post.uri}" th:text="#{article.readmore}">
Lire la suite
</a>
</article>
</div>
</main>
</body>
</html>
index_en.html(英語インデックス) :
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{site.title}">My Blog</title>
</head>
<body>
<main>
<h1 th:text="#{nav.blog}">Blog</h1>
<div class="articles-list">
<article th:each="post : ${published_posts}"
th:if="${post.lang == 'en'}">
<h2>
<a th:href="${post.uri}" th:text="${post.title}">Title</a>
</h2>
<time th:text="${#dates.format(post.date, 'dd MMMM yyyy')}">
Date
</time>
<p th:text="${post.description}">Description</p>
<a th:href="${post.uri}" th:text="#{article.readmore}">
Read more
</a>
</article>
</div>
</main>
</body>
</html>
言語間ナビゲーション
グローバル言語セレクター
フローチャート - ユーザーによる読み取り
@startuml
start
:Utilisateur accède au site;
if (Langue préférée ?) then (FR)
:Afficher /index.html;
:Lister articles FR;
else (EN)
:Afficher /en/index.html;
:Lister articles EN;
endif
:Utilisateur clique sur article;
:Afficher article\navec métadonnées;
if (Traductions disponibles ?) then (oui)
:Afficher sélecteur\nde traductions;
if (Changement de langue ?) then (oui)
:Rediriger vers\ntraduction;
:Afficher article\ndans nouvelle langue;
else (non)
:Continuer lecture;
endif
else (non)
:Continuer lecture;
endif
stop
@enduml
メインテンプレートに言語セレクターを追加してください:
<nav class="language-switcher">
<a href="/index.html"
th:classappend="${content.lang == 'fr'} ? 'active'"
title="Français">
🇫🇷 FR
</a>
<a href="/en/index.html"
th:classappend="${content.lang == 'en'} ? 'active'"
title="English">
🇬🇧 EN
</a>
</nav>
セレクター用のCSSスタイル
.language-switcher {
display: flex;
gap: 1rem;
padding: 0.5rem;
background: #f5f5f5;
border-radius: 4px;
}
.language-switcher a {
padding: 0.5rem 1rem;
text-decoration: none;
color: #333;
border-radius: 4px;
transition: background 0.2s;
}
.language-switcher a:hover {
background: #e0e0e0;
}
.language-switcher a.active {
background: #007bff;
color: white;
}
.translations {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-left: 4px solid #007bff;
}
.language-list {
display: inline-flex;
gap: 0.5rem;
list-style: none;
padding: 0;
margin: 0;
}
.language-list li::after {
content: "•";
margin-left: 0.5rem;
}
.language-list li:last-child::after {
content: "";
}
言語別 RSS フィード
言語別にRSSフィードを分けるには、個別のテンプレートを作成してください:
feed.xml(フランス語のフラックス) :
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:th="http://www.thymeleaf.org">
<channel>
<title th:text="#{site.title}">Mon Blog</title>
<link th:text="${config.site_host}">http://example.com</link>
<description th:text="#{site.description}">Description</description>
<language>fr</language>
<item th:each="post : ${published_posts}"
th:if="${post.lang == 'fr'}">
<title th:text="${post.title}">Titre</title>
<link th:text="${config.site_host + post.uri}">Lien</link>
<pubDate th:text="${#dates.format(post.date, 'EEE, dd MMM yyyy HH:mm:ss Z')}">
Date
</pubDate>
<description th:text="${post.description}">Description</description>
</item>
</channel>
</rss>
良いプラクティスとコツ
記事識別子の一貫性
必ず…してください`:jbake-article-id:`すべての同じ記事の翻訳に対して同じです。一貫したフォーマットを使用してください :
-
普遍性のために英語の識別子を使用してください
-
単語を区切るためにハイフンを使用してください
-
特殊文字を避けてください
2. 一貫性のある日付
記事のすべての翻訳は、同じ公開日である必要があります (:jbake-date:). これは並び替えと時系列表示を容易にします。
3. 多言語タグ
タグについては、2つのオプションがあります。
オプション1 : 英語の普遍的なタグ
:jbake-tags: java, spring-boot, microservices
オプション2: マッピングを使ってタグを翻訳
# Version française
:jbake-tags: java, spring-boot, microservices
# Version anglaise
:jbake-tags: java, spring-boot, microservices
4. 未翻訳記事の管理
すべての記事を訳す必要はありません。ある記事が一方の言語だけに存在する場合、他方の言語のリストには表示されません。
5. 多言語サイトマップ
すべての言語を含むサイトマップを生成してください :
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<url th:each="post : ${published_posts}">
<loc th:text="${config.site_host + post.uri}">URL</loc>
<lastmod th:text="${#dates.format(post.date, 'yyyy-MM-dd')}">Date</lastmod>
<!-- Liens alternatifs pour les traductions -->
<xhtml:link th:each="translation : ${published_posts}"
th:if="${translation['article-id'] == post['article-id'] and translation.lang != post.lang}"
rel="alternate"
th:attr="hreflang=${translation.lang},href=${config.site_host + translation.uri}" />
</url>
</urlset>
結論
JBakeとThymeleafを使用したサイトの国際化は、堅牢で保守しやすいアプローチです。テンプレート(メッセージファイルを通じて)とコンテンツ(フォルダ構成を通じて)の国際化を分離することで、柔軟で容易に拡張できるシステムを得ることができます。
覚えておくべき重要なポイント :
-
Thymeleafメッセージファイルユーザーインターフェイスのために
-
フォルダ別の整理(年/言語) の記事
-
記事ID翻訳をリンクするために
-
�専用テンプレート各言語に対して
-
明示的なURL言語コードを含む
配置図
@startuml
node "開発マシン" {
artifact "ソース" {
folder "コンテンツ/"
folder "テンプレート/"
folder "assets/"
}
component "JBake CLI" as jbake
Sources --> jbake : jbake -b
}
node "ビルドサーバー
(CI/CD)" {
component "GitHub Actions
または GitLab CI" as ci
jbake --> ci : push
}
cloud "CDN / ホスティング" {
node "静的Webサーバー" {
artifact "生成されたサイト" {
folder "/fr/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "生成されたサイト" : déploiement
actor "読者" as users
users --> "静的Webサーバー" : HTTPS
@enduml
このアーキテクチャは、2つの言語でシンプルに始められ、大規模なリファクタリングなしに他の言語を追加することもできます。全体は完全に静的で高性能であり、JBakeの哲学に忠実です。
良い多言語開発! 🌍
関連記事
31 May 2026
14 May 2026