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 :

  1. internasionalisasi templatingpenggunaan file pesan Thymeleaf untuk elemen antarmuka

  2. 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 ! 🌍

Artikel terkait