1. Introduction

L’internationalisation (i18n) d’un site statique peut sembler complexe au premier abord, mais JBake combiné avec Thymeleaf offre des solutions élégantes pour créer un site multilingue. Dans cet article, je vais vous montrer comment j’ai mis en place l’i18n sur mon blog, en couvrant à la fois le templating et la gestion des articles dans plusieurs langues.

1.1. Diagramme de cas d’usage (Use Case)

Diagram

2. Architecture de l’internationalisation

Notre approche repose sur deux piliers :

  1. L’i18n du templating : utilisation des fichiers de messages Thymeleaf pour les éléments d’interface

  2. L’i18n du contenu : organisation des articles par langue dans une structure de dossiers dédiée

2.1. Diagramme de structure (Organisation des fichiers)

Diagram

2.2. Pourquoi cette approche ?

Cette séparation permet de :

  • Maintenir une cohérence dans l’interface quelle que soit la langue

  • Gérer indépendamment le contenu et les traductions d’articles

  • Faciliter l’ajout de nouvelles langues sans refactoring majeur

  • Permettre des articles disponibles uniquement dans certaines langues

2.3. Diagramme de composants

Diagram

3. I18n du templating avec Thymeleaf

3.1. Structure des fichiers de messages

La première étape consiste à créer les fichiers de propriétés pour chaque langue supportée :

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

3.2. Contenu des fichiers de messages

Voici un exemple de fichier 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...

Et son équivalent anglais 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...

3.3. Utilisation dans les templates

Dans vos templates Thymeleaf, utilisez la syntaxe #{} pour accéder aux messages :

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

3.4. Diagramme de séquence - Résolution i18n

Diagram

3.5. Configuration de la locale dans JBake

Dans votre fichier jbake.properties, définissez la locale par défaut :

# Locale par défaut
thymeleaf.locale=fr

# Encodage
template.encoding=UTF-8

4. I18n des articles : organisation par dossiers

4.1. Diagramme de flux (Flow) - Génération

Diagram

4.2. Structure des dossiers

Plutôt que d’utiliser des suffixes dans les noms de fichiers, j’ai opté pour une organisation par dossiers qui offre plus de clarté et de maintenabilité :

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

4.3. Avantages de cette approche

Cette structure présente plusieurs avantages :

  • Séparation claire : chaque langue a son propre espace

  • Nommage flexible : les fichiers peuvent avoir des noms différents selon la langue

  • Scalabilité : facile d’ajouter une nouvelle langue

  • Organisation naturelle : suit la logique temporelle de JBake

4.4. Métadonnées des articles

Chaque article doit contenir des métadonnées pour permettre la liaison entre traductions. Voici un exemple :

Version française (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

Version anglaise (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
L’attribut :jbake-article-id: est crucial : il permet de lier les différentes traductions d’un même article.

4.5. Configuration des URLs

Dans jbake.properties, configurez le pattern d’URL pour inclure la langue :

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

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

Cela génèrera des URLs du type :

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

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

5. Templates pour l’affichage multilingue

5.1. Template d’article avec sélecteur de langue

Créez un template post.html qui affiche les traductions disponibles :

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

5.2. Index filtré par langue

Créez des templates d’index pour chaque langue :

index.html (index français) :

<!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 (index anglais) :

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

6. Navigation entre langues

6.1. Sélecteur de langue global

6.2. Diagramme de flux - Lecture utilisateur

Diagram

Ajoutez un sélecteur de langue dans votre template principal :

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

6.3. Style CSS pour le sélecteur

.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: "";
}

7. Flux RSS par langue

Pour avoir des flux RSS séparés par langue, créez des templates distincts :

feed.xml (flux français) :

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

8. Bonnes pratiques et astuces

8.1. 1. Cohérence des identifiants d’article

Assurez-vous que :jbake-article-id: est identique pour toutes les traductions d’un même article. Utilisez un format cohérent :

  • Préférez les identifiants en anglais pour universalité

  • Utilisez des tirets pour séparer les mots

  • Évitez les caractères spéciaux

8.2. 2. Dates cohérentes

Toutes les traductions d’un article doivent avoir la même date de publication (:jbake-date:). Cela facilite le tri et l’affichage chronologique.

8.3. 3. Tags multilingues

Pour les tags, vous avez deux options :

Option 1 : Tags universels en anglais

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

Option 2 : Tags traduits avec mapping

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

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

8.4. 4. Gestion des articles non traduits

Il n’est pas obligatoire de traduire tous les articles. Si un article n’existe que dans une langue, il n’apparaîtra simplement pas dans les listings de l’autre langue.

8.5. 5. Sitemap multilingue

Générez un sitemap qui inclut toutes les langues :

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

9. Conclusion

L’internationalisation d’un site JBake avec Thymeleaf est une approche robuste et maintenable. En séparant l’i18n du templating (via les fichiers de messages) et l’i18n du contenu (via l’organisation en dossiers), vous obtenez un système flexible qui peut évoluer facilement.

Les points clés à retenir :

  • Fichiers de messages Thymeleaf pour l’interface utilisateur

  • Organisation par dossiers (année/langue) pour les articles

  • Identifiants d’article pour lier les traductions

  • Templates dédiés pour chaque langue

  • URLs explicites incluant le code de langue

9.1. Diagramme de déploiement

Diagram

Cette architecture vous permet de démarrer simplement avec deux langues et d’en ajouter d’autres sans refactoring majeur. Le tout reste entièrement statique et performant, fidèle à la philosophie de JBake.

Bon développement multilingue ! 🌍