介�绍

静态网站的国际化(i18n)乍看起来可能很复杂,但JBake结合Thymeleaf提供了创建多语言站点的优雅解决方案。在本文中,我将展示我如何在我的博客上实现i18n,涵盖模板和多语言文章管理。

用例图 (Use Case)

Diagram

国际化架构

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

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

  2. 内容的 i18n按语言组织文章,放置在专用的文件夹结构中

结构图(文件组织)

Diagram

为什么采用这种方法?

这种分离可以:

  • 保持界面一致性,无论语言如何

  • 独立管理文章的内容和翻译

  • 简化添加新语言,无需进行大规模重构

  • 允许仅在特定语言中可用的文章

组件图

Diagram

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>

时序图 - 国际化解决方案

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 4) ]

@startuml
actor Auteur
participant JBake
participant "AsciiDoc
^^^^^
 Syntax Error? (Assumed diagram type: sequence)

@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) - 生成

Diagram

文件夹结构

与其在文件名中使用后缀,我选择了按文件夹组织的方式,这样更清晰且易于维护:

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>

语言之间的导航

全局语言选择器

流程图 - 用户读取

Diagram

在你的主模板中添加语言选择器:

<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包含语言代码

部署图

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 15) ]

@startuml
node "开发机器" {
  artifact "来源" {
    folder "content/"
    folder "模板/"
    folder "资产/"
  }

  component "JBake 命令行界面" as jbake

  Sources --> jbake : jbake -b
}

node "构建服务器\n(CI/CD)" {
  component "GitHub Actions
^^^^^
 Syntax Error? (Assumed diagram type: component)

@startuml
node "开发机器" {
  artifact "来源" {
    folder "content/"
    folder "模板/"
    folder "资产/"
  }

  component "JBake 命令行界面" as jbake

  Sources --> jbake : jbake -b
}

node "构建服务器\n(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

这种架构让您可以简单地使用两种语言开始,并且可以在不进行大规模重构的情况下添加更多语言。整个系统保持完全静态且高性能,忠实于JBake的理念。

祝您多语言开发顺利!🌍

相关文章