介�绍

静态网站的国际化(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

国际化架构

我们的方法基于两个支柱:

  1. 模板的国际化: 使用 Thymeleaf 消息文件用于界面元素

  2. 内容的 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的理念。

祝您多语言开发顺利!🌍

相关文章