Introduzione

L’internationalizzazione (i18n) di un sito statico può sembrare complesso a prima vista, ma JBake combinato con Thymeleaf offre soluzioni eleganti per creare un sito multilingue. In questo articolo, vi mostrerò come ho implementato l’i18n sul mio blog, coprendo sia il templating che la gestione degli articoli in più lingue.

Diagramma di casi d’uso (Use Case)

Diagram

Architettura dell’internazionalizzazione

Il nostro approccio si basa su due pilastri:

  1. L’internazionalizzazione del templatingutilizzo dei file di messaggi Thymeleaf per gli elementi dell’interfaccia

  2. L’i18n del contenutoorganizzazione degli articoli per lingua in una struttura di cartelle dedicata

Diagramma di struttura (Organizzazione dei file)

Diagram

Perché questo approccio?

Questa separazione permette di:

  • Mantenere la coerenza nell’interfaccia indipendentemente dalla lingua

  • Gestire indipendentemente il contenuto e le traduzioni degli articoli

  • Facilitare l’aggiunta di nuove lingue senza un refactoring importante

  • Consentire articoli disponibili solo in alcune lingue

Diagramma dei componenti

Diagram

I18n del templating con Thymeleaf

Struttura dei file di messaggi

Il primo passaggio consiste nel creare i file di proprietà per ogni lingua supportata :

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

Contenuto dei file dei messaggi

Ecco un esempio di file`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...

E il suo equivalente inglese`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...

Utilizzo nei template

Nei vostri template Thymeleaf, utilizza la sintassi`#{}`per accedere ai messaggi :

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

Diagramma di sequenza - Risoluzione i18n

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
Parser" as parser
participant "Thymeleaf
Engine" 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: "Casa"
i18n --> thymeleaf: "Casa"
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: "Casa"
i18n --> thymeleaf: "Casa"
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

Configurazione della locale in JBake

Nel tuo file`jbake.properties`, imposta il locale predefinito :

# Locale par défaut
thymeleaf.locale=fr

# Encodage
template.encoding=UTF-8

I18n degli articoli: organizzazione per cartelle

Diagramma di flusso (Flow) - Generazione

Diagram

Struttura delle cartelle

Invece di utilizzare suffissi nei nomi dei file, ho optato per una organizzazione per cartelle che offre maggiore chiarezza e manutenibilità :

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

Vantaggi di questo approccio

Questa struttura presenta diversi vantaggi:

  • Separazione chiara: ogni lingua ha il suo spazio

  • Denominazione flessibileI file possono avere nomi diversi a seconda della lingua

  • Scalabilità: facile da aggiungere una nuova lingua

  • organizzazione naturale: segue la logica temporale di JBake

Metadati degli articoli

Ogni articolo deve contenere dei metadati per consentire il collegamento tra le traduzioni. Ecco un esempio :

Versione francese(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

Versione inglese(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

NOTA: L’attributo`:jbake-article-id:`è cruciale : permette di collegare le diverse traduzioni di uno stesso articolo.

Configurazione degli URL

In`jbake.properties`, configura il pattern dell’URL per includere la lingua:

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

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

Ciò genererà URL del tipo :

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

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

Template per la visualizzazione multilingue

Modello di articolo con selettore di lingua

Create un template`post.html`che mostra le traduzioni disponibili:

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

Indice filtrato per lingua

Crea i template di indice per ogni lingua:

index.html(indice italiano) :

<!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(indice inglese) :

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

Navigazione tra lingue

Selezione lingua globale

Diagramma di flusso - Lettura utente

Diagram

Aggiungi un selettore di lingua nel tuo template principale:

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

Stile CSS per il selettore

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

Feed RSS per lingua

Create dei template distinti:

feed.xml(flusso francese) :

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

Buone pratiche e consigli

1. Coerenza degli identificatori dell’articolo

Assicurati che`:jbake-article-id:`è identico per tutte le traduzioni dello stesso articolo. Utilizza un formato coerente :

  • Preferisci gli identificatori in inglese per universalità

  • Utilizzate dei trattini per separare le parole

  • Evitate i caratteri speciali

2. Date coerenti

Tutte le traduzioni di un articolo devono avere la stessa data di pubblicazione.:jbake-date:). Ciò facilita la classificazione e la visualizzazione cronologica.

3. Tag multilingue

Per i tag, hai due opzioni :

Opzione 1: Tag universali in inglese

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

Option 2 : Tag tradotti con mapping

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

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

4. Gestione degli articoli non tradotti

Non è obbligatorio tradurre tutti gli articoli. Se un articolo esiste solo in una lingua, semplicemente non apparirà negli elenchi dell’altra lingua.

5. Sitemap multilingue

Genera una sitemap che includa tutte le lingue :

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

Conclusione

L’internazionalizzazione di un sito JBake con Thymeleaf è un approccio robusto e mantenibile. Separando l’i18n dal templating (tramite i file dei messaggi) e l’i18n dal contenuto (tramite l’organizzazione in cartelle), si ottiene un sistema flessibile che può evolvere facilmente.

I punti chiave da ricordare:

  • File di messaggi Thymeleafper l’interfaccia utente

  • Organizzazione per cartelle(anno/lingua) per gli articoli

  • identificatori articoloper collegare le traduzioni

  • Modelli dedicatiper ogni lingua

  • URL espliciteincluse il codice della lingua

Diagramma di distribuzione

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

@startuml
node "Macchina di sviluppo" {
  artifact "Fonti" {
    folder "</think>"
    folder "templates/"
    folder "attivi/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "Server di build
^^^^^
 Syntax Error? (Assumed diagram type: component)

@startuml
node "Macchina di sviluppo" {
  artifact "Fonti" {
    folder "</think>"
    folder "templates/"
    folder "attivi/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "Server di build
(CI/CD)" {
  component "GitHub Actions
o GitLab CI" as ci

  jbake --> ci : push
}

cloud "CDN / Hosting" {
  node "Server Web statico" {
    artifact "Sito Generato" {
      folder "/it/blog/"
      folder "/en/blog/"
      folder "/assets/"
    }
  }
}

ci --> "Sito Generato" : déploiement

actor "Lettori" as users

users --> "Server Web statico" : HTTPS
@enduml

Questa architettura ti permette di iniziare semplicemente con due lingue e di aggiungerne altre senza un refactoring importante. Il tutto rimane completamente statico e performante, fedele alla filosofia di JBake.

Buon sviluppo multilingue ! 🌍

Articoli correlati