Einleitung

Die Internationalisierung (i18n) einer statischen Seite kann zunächst komplex erscheinen, doch JBake in Kombination mit Thymeleaf bietet elegante Lösungen für die Erstellung einer mehrsprachigen Website. In diesem Artikel zeige ich dir, wie ich die i18n auf meinem Blog implementiert habe, dabei sowohl das Templating als auch die Verwaltung von Artikeln in mehreren Sprachen abgedeckt habe.

Use-Case-Diagramm (Use Case)

@startuml
left to right direction
skinparam packageStyle rectangle

actor "Autor" as author
actor "Leser FR" as reader_fr
actor "Leser EN" as reader_en

rectangle "Mehrsprachige JBake-Seite" {
  usecase "Artikel FR schreiben" as UC1
  usecase "Artikel EN schreiben" as UC2
  usecase "i18n-Nachrichten konfigurieren" as UC3
  usecase "Seite generieren" as UC4
  usecase "Artikel FR lesen" as UC5
  usecase "Artikel EN lesen" as UC6
  usecase "Sprache ändern" as UC7
  usecase "Verfügbare Übersetzungen anzeigen" 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

Architektur der Internationalisierung

Unser Ansatz beruht auf zwei Säulen:

  1. Die i18n des Templatings: Verwendung der Thymeleaf-Nachrichtendateien für UI-Elemente

  2. Die i18n des InhaltsOrganisation der Artikel nach Sprache in einer dedizierten Ordnerstruktur

Strukturdiagramm (Dateiorganisation)

@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

Warum dieser Ansatz?

Diese Trennung ermöglicht:

  • Eine Konsistenz in der Benutzeroberfläche unabhängig von der Sprache aufrechterhalten

  • Den Inhalt und die Artikelübersetzungen unabhängig verwalten

  • Erleichtern das Hinzufügen neuer Sprachen ohne umfangreiches Refactoring

  • Ermögliche Artikel, die nur in bestimmten Sprachen verfügbar sind.

Komponentendiagramm

@startuml
skinparam componentStyle rectangle

package "Quellen" {
  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 "templates/" {
    [post.html]
    [index.html]
    [index_en.html]
  }

  folder "messages/" {
    [messages_fr.properties]
    [messages_en.properties]
  }

  [jbake.properties]
}

package "JBake Engine" {
  [Parser AsciiDoc] as parser
  [Template Engine\nThymeleaf] as thymeleaf
  [Generator] as generator
  [I18n Resolver] as i18n
}

package "Generierte Seite" {
  folder "fr/" {
    folder "blog/2025/" as blog_fr {
      [article1.html]
      [article2.html]
    }
  }

  folder "en/" {
    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

I18n des Templatings mit Thymeleaf

Struktur der Nachrichten­dateien

Der erste Schritt besteht darin, die Eigenschaftendateien für jede unterstützte Sprache zu erstellen :

src/jbake/templates/
├── messages.properties        # Fallback par défaut
├── messages_fr.properties     # Français
├── messages_en.properties     # Anglais
└── messages_de.properties     # Allemand

Inhalt der Nachrichtendateien

Hier ist ein Beispiel einer Datei`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...

und sein englisches Äquivalent`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...

Verwendung in den Vorlagen

In Ihren Thymeleaf-Vorlagen, verwenden Sie die Syntax`#{}`um auf die Nachrichten zuzugreifen :

<!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>

Sequenzdiagramm - i18n-Auflösung

@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: 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: "Startseite"
i18n --> thymeleaf: "Startseite"
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: "Home"
i18n --> thymeleaf: "Home"
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

Konfiguration der Locale in JBake

In Ihrer Datei`jbake.properties`, legen Sie das Gebietsschema fest :

# Locale par défaut
thymeleaf.locale=fr

# Encodage
template.encoding=UTF-8

I18n der Artikel: Organisation nach Ordner

Flussdiagramm (Flow) - Generierung

@startuml
start

:Lire jbake.properties;
:Charger configuration i18n;

partition "Für jede AsciiDoc-Datei" {
  :Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
  :Parser contenu AsciiDoc;
  :Stocker en mémoire avec langue;
}

partition "Für jedes Template" {
  :Charger template Thymeleaf;
  :Identifier langue cible;
  :Charger messages_{lang}.properties;

  partition "Filterung des Inhalts" {
    :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

Ordnerstruktur

Anstatt Suffixe in den Dateinamen zu verwenden, habe ich mich für eine Organisation nach Ordnern entschieden, die mehr Klarheit und Wartbarkeit bietet:

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

Vorteile dieses Ansatzes

Diese Struktur bietet mehrere Vorteile:

  • klare Trennung: jede Sprache hat ihren eigenen Raum

  • flexible Benennung: Die Dateien können je nach Sprache unterschiedliche Namen haben.

  • Skalierbarkeit: einfach eine neue Sprache hinzuzufügen

  • Natürliche Organisation: folgt der zeitlichen Logik von JBake

Metadaten der Artikel

Jeder Artikel muss Metadaten enthalten, um die Verknüpfung zwischen Übersetzungen zu ermöglichen. Hier ist ein Beispiel:

Französische Version(2025/fr/internationalisation-jbake.adoc) :

= Internationalisation d'un site statique JBake
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: de
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide pour mettre en place l'i18n avec JBake

Englische Version (2025/en/jbake-internationalization.adoc) :

= Internationalizing a JBake Static Site
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: de
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide to implement i18n with JBake

HINWEIS: Das Attribut`:jbake-article-id:`es ist entscheidend: es ermöglicht, die verschiedenen Übersetzungen desselben Artikels zu verknüpfen.

Konfiguration der URLs

in`jbake.properties`, konfigurieren Sie das URL-Muster, um die Sprache einzuschließen:

# Pattern d'URL avec langue
post.permalink.pattern=:lang/blog/:year/:name.html

# Langue par défaut
site.default.lang=fr

Dies erzeugt URLs des Typs:

  • /fr/blog/2025/internationalisation-jbake.html

  • /en/blog/2025/jbake-internationalization.html

Vorlagen für die mehrsprachige Anzeige

Artikelvorlage mit Sprachauswähler

Erstellen Sie eine Vorlage`post.html`der die verfügbaren Übersetzungen anzeigt :

<!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>

Nach Sprache gefilterter Index

Erstellen Sie Indexvorlagen für jede Sprache:

index.html(französischer Index) :

<!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(englischer Index) :

<!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>

Navigation zwischen Sprachen

Globale Sprachauswahl

Flussdiagramm - Benutzerlesen

@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

Fügen Sie Ihrem Haupt-Template einen Sprachauswähler hinzu:

<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-Stil für den Selektor

.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-Feed nach Sprache

Um getrennte RSS-Feeds nach Sprache zu haben, erstelle separate Vorlagen:

feed.xml(französischer Fluss) :

<?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>

Gute Praktiken und Tipps

1. Konsistenz der Artikel-IDs

Stellen Sie sicher, dass`:jbake-article-id:`ist für alle Übersetzungen eines Artikels identisch. Verwenden Sie ein einheitliches Format:

  • Bevorzugen Sie englische Bezeichner für Universalität

  • Verwende Bindestriche, um die Wörter zu trennen

  • Vermeiden Sie Sonderzeichen

2. Konsistente Daten

Alle Übersetzungen eines Artikels müssen das gleiche Veröffentlichungsdatum haben (:jbake-date:). Dies erleichtert das Sortieren und die chronologische Anzeige.

3. Mehrsprachige Tags

Für die Tags, haben Sie zwei Optionen:

Option 1: Universelle Tags auf Englisch

:jbake-tags: java, spring-boot, microservices

Option 2 : Tags übersetzt mit Zuordnung

# Version française
:jbake-tags: java, spring-boot, microservices

# Version anglaise
:jbake-tags: java, spring-boot, microservices

4. Verwaltung der nicht übersetzten Artikel

Es ist nicht verpflichtend, alle Artikel zu übersetzen. Wenn ein Artikel nur in einer Sprache existiert, erscheint er einfach nicht in den Auflistungen der anderen Sprache.

5. Mehrsprachige Sitemap

Generieren Sie einen Sitemap, der alle Sprachen umfasst:

<?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>

Fazit

Die Internationalisierung einer JBake-Site mit Thymeleaf ist ein robuster und wartbarer Ansatz. Durch die Trennung von i18n im Templating (über die Message-Dateien) und i18n im Inhalt (durch die Ordnerorganisation) erhalten Sie ein flexibles System, das sich leicht weiterentwickeln lässt.

Die wichtigsten zu beachtenden Punkte:

  • Thymeleaf-Nachrichtendateienfür die Benutzeroberfläche

  • Ordnerbasierte Organisation(Jahr/Sprache) für die Artikel

  • Artikel-IDsum die Übersetzungen zu verlinken

  • dedizierte Vorlagenfür jede Sprache

  • Explizite URLsinklusive des Sprachcodes

Bereitstellungsdiagramm

@startuml
node "Entwicklungsmaschine" {
  artifact "Quellen" {
    folder "content/"
    folder "templates/"
    folder "assets/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "Build-Server\n(CI/CD)" {
  component "GitHub Actions\noder GitLab CI" as ci

  jbake --> ci : push
}

cloud "CDN / Hosting" {
  node "Statischer Webserver" {
    artifact "Generierte Website" {
      folder "/fr/blog/"
      folder "/en/blog/"
      folder "/assets/"
    }
  }
}

ci --> "Generierte Website" : déploiement

actor "Leser" as users

users --> "Statischer Webserver" : HTTPS
@enduml

Diese Architektur ermöglicht es Ihnen, einfach mit zwei Sprachen zu beginnen und weitere hinzuzufügen, ohne ein großes Refactoring durchführen zu müssen. Alles bleibt vollständig statisch und leistungsfähig und bleibt der Philosophie von JBake treu.

Gute mehrsprachige Entwicklung! 🌍

Verwandte Artikel