使用 Thymeleaf 对静态 JBake 网站进行国际化
发布于 20 October 2025
介�绍
静态网站的国际化(i18n)乍看起来可能很复杂,但JBake结合Thymeleaf提供了创建多语言站点的优雅解决方案。在本文中,我将展示我如何在我的博客上实现i18n,涵盖模板和多语言文章管理。
用例图 (Use Case)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "作者" as author
actor "法语阅读器" as reader_fr
actor "英文阅读器" as reader_en
rectangle "JBake 多语言网站" {
usecase "撰写法语文章" as UC1
usecase "写文章英文" as UC2
usecase "配置 i18n 消息" as UC3
usecase "生成网站" as UC4
usecase "阅读文章(法语)" as UC5
usecase "阅读文章英文版" 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
国际化架构
我们的方法基于两个支柱:
-
模板的国际化: 使用 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 "在/" 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 "blog/2025/" as blog_fr {
[article1.html]
[article2.html]
}
}
folder "在/" {
folder "blog/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>
时序图 - 国际化解决方案
@startuml
actor Auteur
participant JBake
participant "AsciiDoc\nParser" as parser
participant "Thymeleaf\nEngine" as thymeleaf
participant "I18n\nResolver" as i18n
database "messages_*.properties" as messages
Auteur -> JBake: jbake -b
activate JBake
JBake -> parser: 读取 article.fr.adoc
activate parser
parser --> JBake: 内容 + 元数据\n(lang=fr, article-id=xyz)
deactivate parser
JBake -> parser: 读取 article.en.adoc
activate parser
parser --> JBake: 内容 + 元数据\n(lang=en, article-id=xyz)
deactivate parser
JBake -> thymeleaf: 生成 FR 页面
activate thymeleaf
thymeleaf -> i18n: 求解 #{nav.home}
activate i18n
i18n -> messages: 读取 messages_fr.properties
messages --> i18n: "首页"
i18n --> thymeleaf: "首页"
deactivate i18n
thymeleaf -> JBake: 搜索翻译\n(article-id=xyz, lang!=fr)
JBake --> thymeleaf: article.en.html trouvé
thymeleaf --> JBake: /fr/blog/article.html
deactivate thymeleaf
JBake -> thymeleaf: 生成 EN 页面
activate thymeleaf
thymeleaf -> i18n: 求解 #{nav.home}
activate i18n
i18n -> messages: 读取 messages_en.properties
messages --> i18n: "主页"
i18n --> thymeleaf: "主页"
deactivate i18n
thymeleaf -> JBake: 搜索翻译\n(article-id=xyz, lang!=en)
JBake --> thymeleaf: article.fr.html 已找到
thymeleaf --> JBake: /en/blog/article.html
deactivate thymeleaf
JBake --> Auteur: 生成的站点
deactivate JBake
@enduml
JBake 的语言环境配置
在您的文件中`jbake.properties`, 设置默认语言环境 :
# Locale par défaut
thymeleaf.locale=fr
# Encodage
template.encoding=UTF-8
文章国际化:按文件夹组织
流程图 (Flow) - 生成
@startuml
start
:读取 jbake.properties;
:充电器配置国际化;
partition "对于每个文件 AsciiDoc" {
:读取元数据\n(:jbake-lang:, :jbake-article-id:);
:解析内容 AsciiDoc;
:使用语言存储于内存中;
}
partition "对于每个模板" {
:加载模板 Thymeleaf;
:目标语言标识符;
:加载 messages_{lang}.properties;
partition "内容过滤" {
:按语言筛选文章;
:按文章ID分组翻译;
}
:使用带有 i18n 的模板;
:生成 HTML 在 /{lang}/;
}
:Copier assets statiques;
:生成多语言站点地图;
:按语言生成RSS流;
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: zh
: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: zh
: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
:用户访问该网站;
if (Langue préférée ?) then (FR)
:显示 /index.html;
:列出FR文章;
else (EN)
:显示 /en/index.html;
:列出英文文章;
endif
:用户点击文章;
:显示文章\n带有元数据;
if (Traductions disponibles ?) then (oui)
:显示选择器\n 的翻译;
if (Changement de langue ?) then (oui)
:重定向到\n翻译;
:显示文章\n以新语言;
else (non)
:继续阅读;
endif
else (non)
:继续阅读;
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>
良好实践和技巧
1. 文章标识符的一致性
请确保`:jbake-article-id:`对于同一文章的所有翻译都是相同的。 请使用一致的格式:
-
请使用英文标识符以实现通用性
-
使用短横线来分隔单词
-
避免使用特殊字符
2. 日期一致
一篇文章的所有翻译必须有相同的出版日期`:jbake-date:`). 这有助于排序和按时间顺序显示。
3. 多语言标签
对于标签,您有两种选择:
选项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>
结论
使用 Thymeleaf 对 JBake 站点进行国际化是一种健壮且可维护的方法。通过将 i18n 与模板分离(通过消息文件)以及将内容的 i18n 通过目录组织来分离,您可以获得一个灵活、易于演进的系统。
要点如下:
-
Thymeleaf 消息文件对于用户界面
-
按文件夹组织(年/语言) 用于文章
-
文章标识符用于链接翻译
-
专用模板对于每种语言
-
显式 URL包含语言代码
部署图
@startuml
node "开发机器" {
artifact "源码" {
folder "content/"
folder "templates/"
folder "assets/"
}
component "JBake CLI" as jbake
Sources --> jbake : jbake -b
}
node "构建服务器\n(CI/CD)" {
component "GitHub 操作\n或 GitLab CI" as ci
jbake --> ci : 推送
}
cloud "CDN / 托管" {
node "静态 Web 服务器" {
artifact "已生成站点" {
folder "/fr/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "已生成站点" : déploiement
actor "阅读器" as users
users --> "静态 Web 服务器" : HTTPS
@enduml
这种架构让您可以简单地使用两种语言开始,并且可以在不进行大规模重构的情况下添加更多语言。整个系统保持完全静态且高性能,忠实于JBake的理念。
祝您多语言开发顺利!🌍
相关文章
31 May 2026
14 May 2026
12 May 2026