Internasionalisasi situs statis JBake dengan Thymeleaf
Diterbitkan 20 October 2025
Pendahuluan
Internasionalisasi (i18n) situs statis dapat terlihat kompleks pada pertama kalinya, tetapi JBake yang dikombinasikan dengan Thymeleaf menawarkan solusi yang anggun untuk membuat situs multilingual. Dalam artikel ini, saya akan menunjukkan bagaimana saya telah mengimplementasikan i18n di blog saya, mencakup baik templating maupun pengelolaan artikel dalam beberapa bahasa.
Diagram kasus penggunaan (Use Case)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "Penulis" as author
actor "Pembaca FR" as reader_fr
actor "Pembaca EN" as reader_en
rectangle "Situs JBake Multibahasa" {
usecase "Tulis artikel FR" as UC1
usecase "Menulis artikel EN" as UC2
usecase "Mengatur pesan i18n" as UC3
usecase "Buat situs" as UC4
usecase "Membaca artikel FR" as UC5
usecase "Baca artikel EN" as UC6
usecase "Ubah bahasa" as UC7
usecase "Lihat terjemahan yang tersedia" 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
Arsitektur internasionalisasi
Pendekatan kami didasarkan pada dua pilar :
-
internasionalisasi templatingpenggunaan file pesan Thymeleaf untuk elemen antarmuka
-
internasionalisasi konten: pengaturan artikel menurut bahasa dalam struktur folder khusus
Diagram struktur (Organisasi file)
@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
Mengapa pendekatan ini?
Pemisahan ini memungkinkan :
-
Pertahankan konsistensi antarmuka apa pun bahasa
-
Mengelola secara independen konten dan terjemahan artikel
-
Memudahkan penambahan bahasa baru tanpa refactoring utama
-
Mengizinkan artikel yang hanya tersedia dalam beberapa bahasa
Diagram komponen
@startuml
skinparam componentStyle rectangle
package "Sumber" {
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 "pesan/" {
[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 "Situs yang dihasilkan" {
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 templating dengan Thymeleaf
Struktur file pesan
Langkah pertama adalah membuat file properti untuk setiap bahasa yang didukung:
src/jbake/templates/
├── messages.properties # Fallback par défaut
├── messages_fr.properties # Français
├── messages_en.properties # Anglais
└── messages_de.properties # Allemand
Isi file pesan
Berikut contoh 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...
Dan setaraannya dalam bahasa Inggris`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...
Penggunaan dalam templat
Dalam template Thymeleaf Anda, gunakan sintaksis`#{}`untuk mengakses pesan :
<!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>
Diagram urutan - resolusi i18n
@startuml
actor Auteur
participant JBake
participant "AsciiDoc\nParser" 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: "Beranda"
i18n --> thymeleaf: "Beranda"
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: "Rumah"
i18n --> thymeleaf: "Rumah"
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
Konfigurasi lokal di JBake
Di file Anda`jbake.properties`, tetapkan locale default :
# Locale par défaut
thymeleaf.locale=fr
# Encodage
template.encoding=UTF-8
Internasionalisasi artikel : organisasi berdasarkan folder
Diagram alir (Flow) - Generasi
@startuml
start
:Lire jbake.properties;
:Charger configuration i18n;
partition "Untuk setiap file AsciiDoc" {
:Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
:Parser contenu AsciiDoc;
:Stocker en mémoire avec langue;
}
partition "Untuk setiap template" {
:Charger template Thymeleaf;
:Identifier langue cible;
:Charger messages_{lang}.properties;
partition "Penyaringan konten" {
: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
Struktur folder
Alih-alih menggunakan ekstensi dalam nama file, saya memilih organisasi berdasarkan folder yang memberikan lebih banyak kejelasan dan kemudahan pemeliharaan:
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
Keuntungan dari pendekatan ini
Struktur ini memiliki beberapa keuntungan :
-
pemisahan yang jelas: setiap bahasa memiliki ruang sendiri
-
penamaan fleksibel: file dapat memiliki nama yang berbeda tergantung pada bahasa
-
Skalabilitas: mudah menambahkan bahasa baru
-
Organisasi alamiah: mengikuti logika waktu JBake
Metadata artikel
Setiap artikel harus mengandung metadata untuk memungkinkan hubungan antara terjemahan. Ini adalah contoh :
Versi Prancis(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
Versi bahasa Inggris (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
CATATAN: Atribut`:jbake-article-id:`est penting : ia memungkinkan untuk menghubungkan berbagai terjemahan artikel yang sama.
Konfigurasi URL
di dalam`jbake.properties`, atur pola URL untuk mencakup bahasa :
# Pattern d'URL avec langue
post.permalink.pattern=:lang/blog/:year/:name.html
# Langue par défaut
site.default.lang=fr
Akan menghasilkan URL bertipe :
-
/fr/blog/2025/internationalisation-jbake.html -
/en/blog/2025/jbake-internationalization.html
Templates untuk tampilan multibahasa
Template artikel dengan pemilih bahasa
Buat template`post.html`yang menampilkan terjemahan yang tersedia :
<!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>
Indeks difilter berdasarkan bahasa
Buat templat indeks untuk setiap bahasa :
index.html(indeks Perancis) :
<!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(indeks bahasa Inggris) :
<!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>
Navigasi antara bahasa
Pemilih bahasa global
Diagram alur - Pembacaan pengguna
@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
Tambahkan pemilih bahasa di template utama Anda:
<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>
Gaya CSS untuk penilih
.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: "";
}
Umpan RSS per bahasa
Untuk memiliki feed RSS terpisah per bahasa, buat template yang terpisah :
feed.xml(aliran Prancis) :
<?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>
Praktik terbaik dan tips
1. Konsistensi ID artikel
Pastikan bahwa`:jbake-article-id:`adalah sama untuk semua terjemahan dari artikel yang sama. Gunakan format yang konsisten :
-
Pilih identifier dalam bahasa Inggris untuk universalitas
-
Gunakan tanda hubung untuk memisahkan kata
-
Hindari karakter khusus
2. Tanggal konsisten
Semua terjemahan suatu artikel harus memiliki tanggal publikasi yang sama (:jbake-date:). Ini memudahkan pengurutan dan penampilan kronologis.
3. Tags multibahasa
Untuk tag, Anda memiliki dua opsi :
Option 1: tag universal bahasa Inggris
:jbake-tags: java, spring-boot, microservices
Opsi 2 : Tag diterjemahkan dengan pemetaan
# Version française
:jbake-tags: java, spring-boot, microservices
# Version anglaise
:jbake-tags: java, spring-boot, microservices
4. Pengelolaan artikel yang tidak diterjemahkan
Tidak wajib untuk menerjemahkan semua artikel. Jika sebuah artikel hanya ada dalam satu bahasa, artikel tersebut tidak akan muncul dalam daftar bahasa lain.
5. Peta situs multibahasa
Buat sitemap yang mencakup semua bahasa
<?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>
Kesimpulan
Internasionalisasi situs JBake dengan Thymeleaf adalah pendekatan yang kuat dan dapat dipelihara. Dengan memisahkan i18n dari templating (melalui file pesan) dan i18n dari konten (melalui organisasi dalam folder), Anda mendapatkan sistem yang fleksibel yang dapat berkembang dengan mudah.
Poin-poin penting yang harus diingat:
-
File pesan Thymeleafuntuk antarmuka pengguna
-
Organisasi berdasarkan folder(tahun/bahasa) untuk artikel
-
ID artikeluntuk menautkan terjemahan
-
template khususuntuk setiap bahasa
-
URL eksplisitmemasukkan kode bahasa
Diagram penerapan
@startuml
node "Mesin pengembangan" {
artifact "Sumber" {
folder "isi/"
folder "templates/"
folder "aset/"
}
component "JBake CLI" as jbake
Sources --> jbake : jbake -b
}
node "Server build\n(CI/CD)" {
component "GitHub Actions
atau GitLab CI" as ci
jbake --> ci : push
}
cloud "CDN / Penyediaan" {
node "Server web statis" {
artifact "situs yang dihasilkan" {
folder "/id/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "situs yang dihasilkan" : déploiement
actor "Pembaca" as users
users --> "Server web statis" : HTTPS
@enduml
Arsitektur ini memungkinkan Anda untuk memulai dengan dua bahasa dengan mudah dan menambah bahasa lain tanpa refactoring yang besar. Semuanya tetap sepenuhnya statis dan berkinerja, setia kepada filosofi JBake.
Selamat pengembangan multilingue ! 🌍