Internationalisering van een statische JBake-site met Thymeleaf
Gepubliceerd op 20 October 2025
Introductie
De internationalisering (i18n) van een statische site kan in eerste instantie complex lijken, maar JBake gecombineerd met Thymeleaf biedt elegante oplossingen om een meertalige site te maken. In dit artikel laat ik zien hoe ik de i18n op mijn blog heb ingesteld, waarbij ik zowel templating als het beheer van artikelen in meerdere talen behandel.
Diagram van gebruikscases (Use Case)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "Auteur" as author
actor "Lezer FR" as reader_fr
actor "Lezer EN" as reader_en
rectangle "Meertalige JBake-site" {
usecase "Artikel schrijven FR" as UC1
usecase "Artikel schrijven EN" as UC2
usecase "i18n-berichten configureren" as UC3
usecase "Site genereren" as UC4
usecase "Artikel lezen FR" as UC5
usecase "Artikel lezen EN" as UC6
usecase "Taal wijzigen" as UC7
usecase "Beschikbare vertalingen bekijken" 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
Internationalisatie-architectuur
Onze aanpak berust op twee pijlers :
-
De i18n van templating: gebruik van Thymeleaf-berichtbestanden voor de interface-elementen
-
De i18n van de inhoudorganisatie van artikelen per taal in een speciale mapstructuur
Structuurdiagram (Organisatie van de bestanden)
@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
Waarom deze benadering?
Deze scheiding maakt het mogelijk:
-
Behoud een consistentie in de interface, ongeacht de taal.
-
Beheren de inhoud en de vertalingen van artikelen onafhankelijk
-
Het toevoegen van nieuwe talen vergemakkelijken zonder grote refactoring
-
Toestaan artikelen die alleen beschikbaar zijn in bepaalde talen
Componentendiagram
@startuml
skinparam componentStyle rectangle
package "Bronnen" {
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 "Site Gegenereerd" {
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 van templating met Thymeleaf
Structuur van de berichtenbestanden
De eerste stap bestaat erin de properties-bestanden voor elke ondersteunde taal aan te maken :
src/jbake/templates/
├── messages.properties # Fallback par défaut
├── messages_fr.properties # Français
├── messages_en.properties # Anglais
└── messages_de.properties # Allemand
Inhoud van de berichtenbestanden
Hier is een voorbeeld van een bestand`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...
En zijn Engelse equivalent`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...
Gebruik in de sjablonen
Gebruik in uw Thymeleaf-sjablonen de syntaxis`#{}`om toegang te krijgen tot de berichten :
<!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>
Sequentiediagram - i18n-oplossing
@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: "Accueil"
i18n --> thymeleaf: "Accueil"
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
Configuratie van de locale in JBake
In jouw bestand`jbake.properties`, stel de standaardlocale in :
# Locale par défaut
thymeleaf.locale=fr
# Encodage
template.encoding=UTF-8
I18n van de artikelen : organisatie per mappen
Stroomdiagram (Flow) - Generatie
@startuml
start
:Lire jbake.properties;
:Charger configuration i18n;
partition "Voor elk AsciiDoc-bestand" {
:Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
:Parser contenu AsciiDoc;
:Stocker en mémoire avec langue;
}
partition "Voor elk template" {
:Charger template Thymeleaf;
:Identifier langue cible;
:Charger messages_{lang}.properties;
partition "Inhoudsfiltering" {
: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
Mapstructuur
In plaats van het gebruik van suffixen in bestandsnamen, heb ik gekozen voor een organisatie op basis van mappen die meer duidelijkheid en onderhoudbaarheid biedt:
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
Voordelen van deze aanpak
Deze structuur biedt meerdere voordelen:
-
Duidelijke scheiding: elke taal heeft zijn eigen ruimte
-
Flexibele naamgeving: bestanden kunnen verschillende namen hebben volgens de taal
-
Schaalbaarheid: gemakkelijk een nieuwe taal toe te voegen
-
Natuurlijke organisatie: volgt de tijdelijke logica van JBake
Metadata van de artikelen
Elk artikel moet metadata bevatten om de koppeling tussen vertalingen mogelijk te maken. Hier is een voorbeeld:
Franse versie(2025/fr/internationalisation-jbake.adoc) :
= Internationalisation d'un site statique JBake
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: nl
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide pour mettre en place l'i18n avec JBake
Engelse versie(2025/en/jbake-internationalization.adoc) :
= Internationalizing a JBake Static Site
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: nl
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide to implement i18n with JBake
LET OP: Het attribuut`:jbake-article-id:`Het is cruciaal: het maakt het mogelijk om de verschillende vertalingen van hetzelfde artikel te koppelen.
Configuratie van de URLs
in`jbake.properties`, configureer het URL-patroon om de taal op te nemen :
# Pattern d'URL avec langue
post.permalink.pattern=:lang/blog/:year/:name.html
# Langue par défaut
site.default.lang=fr
Dit genereert URL’s van het type :
-
/fr/blog/2025/internationalisation-jbake.html -
/en/blog/2025/jbake-internationalization.html
Sjablonen voor de meertalige weergave
Artikel sjabloon met taalselector
Maak een sjabloon`post.html`die de beschikbare vertalingen toont:
<!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 gefilterd op taal
Creëer indexsjablonen voor elke taal:
index.html(index Frans) :
<!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( Engelse 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>
Navigatie tussen talen
globale taalkiezer
Stroomdiagram - Gebruiker lezen
@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
Voeg een taalselector toe aan uw hoofdtemplate :
<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-stijl voor de selector
.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 per taal
Om aparte RSS-feeds per taal te hebben, maak aparte sjablonen aan :
feed.xml(Franse flux) :
<?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>
Goede praktijken en tips
1. Consistentie van artikelidentificatoren
Zorg ervoor dat`:jbake-article-id:`is identiek voor alle vertalingen van hetzelfde artikel. Gebruik een consistent formaat :
-
Voorkeur voor Engelse identificatoren voor universaliteit
-
Gebruik streepjes om de woorden te scheiden
-
Vermijd speciale tekens
2. Consistente datums
Alle vertalingen van een artikel moeten dezelfde publicatiedatum hebben (:jbake-date:). Dit vergemakkelijkt het sorteren en de chronologische weergave.
3. Meertalige tags
Voor de tags, je hebt twee opties :
Optie 1 : Universele tags in het Engels
:jbake-tags: java, spring-boot, microservices
Optie 2 : Vertaalde tags met mapping
# Version française
:jbake-tags: java, spring-boot, microservices
# Version anglaise
:jbake-tags: java, spring-boot, microservices
4. Beheer van niet-vertaalde artikelen
Het is niet verplicht om alle artikelen te vertalen. Als een artikel alleen in één taal bestaat, wordt het eenvoudigweg niet weergegeven in de lijsten van de andere taal.
5. Meertalige sitemap
Genereer een sitemap die alle talen omvat :
<?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>
Conclusie
De internationalisatie van een JBake-site met Thymeleaf is een robuuste en onderhoudbare aanpak. Door i18n van templating (via berichtbestanden) en i18n van inhoud (via de organisatie in mappen) te scheiden, krijgt u een flexibel systeem dat gemakkelijk kan evolueren.
Belangrijkste punten om te onthouden:
-
Berichtbestanden Thymeleafvoor de gebruikersinterface
-
Organisatie per dossiers(jaar/taal) voor de artikelen
-
Artikel-IDsom de vertalingen te koppelen
-
Gespecialiseerde sjablonenvoor elke taal
-
Expliciete URL’sinclusief de taalcode
Implementatiediagram
@startuml
node "Ontwikkelmachine" {
artifact "Bronnen" {
folder "content/"
folder "templates/"
folder "assets/"
}
component "JBake CLI" as jbake
Sources --> jbake : jbake -b
}
node "Buildserver\n(CI/CD)" {
component "GitHub Actions\nof GitLab CI" as ci
jbake --> ci : push
}
cloud "CDN / Hosting" {
node "Statische Webserver" {
artifact "Gegenereerde site" {
folder "/fr/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "Gegenereerde site" : déploiement
actor "Lezers" as users
users --> "Statische Webserver" : HTTPS
@enduml
Met deze architectuur kunt u eenvoudig beginnen met twee talen en er andere aan toevoegen zonder grote refactorings. Alles blijft volledig statisch en presterend, trouw aan de filosofie van JBake.
Goede meertalige ontwikkeling! 🌍