Giriş

İnternasyonelleştirme (i18n) bir statik sitesi ilk bakışta karmaşık görünebilir, ancak JBake Thymeleaf ile birleştirildiğinde çoklu dil desteği olan bir site oluşturmak için şık çözümler sunar. Bu makalede, blogumda i18n’yi nasıl kurduğumu göstermek, hem şablonlama hem de birden fazla dilde makale yönetimini kapsayacak şekilde ele alacağım.

Kullanım durumu diyagramı (Use Case)

@startuml
left to right direction
skinparam packageStyle rectangle

actor "Yazar" as author
actor "FR Okuyucu" as reader_fr
actor "EN Okuyucu" as reader_en

rectangle "Çok Dilli JBake Sitesi" {
  usecase "Fransızca makale yaz" as UC1
  usecase "Makale EN yaz" as UC2
  usecase "i18n mesajlarını yapılandır" as UC3
  usecase "Site oluştur" as UC4
  usecase "FR makalesini oku" as UC5
  usecase "Makaleyi oku EN" as UC6
  usecase "Dili değiştir" as UC7
  usecase "Mevcut çevirileri gör" 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

Uluslararasılaştırma mimarisi

Yaklaşımız iki temel üzerine kuruludur :

  1. Templating’in i18n’i: Thymeleaf mesaj dosyalarının kullanımı arayüz öğeleri için

  2. İçeriğin i18n’i: dil bazlı makalelerin ayrılmış bir klasör yapısında düzenlenmesi

Yapı şeması (Dosya organizasyonu)

@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

Neden bu yaklaşım?

Bu ayırım izin verirs:

  • Arayüzde dille ne olursa olsun tutarlılığı koruyun

  • içeriği ve makale çevirilerini bağımsız olarak yönet

  • Yeni dillerin eklenmesini büyük bir refaktörleme yapmadan kolaylaştır

  • Belirli dillerde sadece mevcut olan makalelere izin ver

Bileşen diyagramı

@startuml
skinparam componentStyle rectangle

package "Kaynaklar" {
  folder "content/blog/2025/" {
    folder "fr/" as content_fr {
      [article1.adoc]
      [article2.adoc]
    }
    folder "içinde/" as content_en {
      [article1.adoc] as article1_en
      [article2.adoc] as article2_en
    }
  }

  folder "şablonlar/" {
    [post.html]
    [index.html]
    [index_en.html]
  }

  folder "mesajlar/" {
    [messages_fr.properties]
    [messages_en.properties]
  }

  [jbake.properties]
}

package "JBake Motoru" {
  [Parser AsciiDoc] as parser
  [Template Engine\nThymeleaf] as thymeleaf
  [Generator] as generator
  [I18n Resolver] as i18n
}

package "Oluşturulan Site" {
  folder "fr/" {
    folder "blog/2025/" as blog_fr {
      [article1.html]
      [article2.html]
    }
  }

  folder "içinde/" {
    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

Thymeleaf ile şablonlama I18n

Mesaj dosyalarının yapısı

İlk adım, her desteklenen dil için özellik dosyalarını oluşturmaktır :

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

Mesaj dosyalarının içeriği

Bir dosya örneği`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...

Ve onun İngilizce karşılığı`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...

Şablonlarda kullanım

Thymeleaf şablonlarınızda, sözdizimi kullanın`#{}`mesajlara erişmek için:

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

Dizilim diyagramı - i18n çözümü

@startuml
actor Auteur
participant JBake
participant "AsciiDoc\nAyrıştırıcısı" as parser
participant "Thymeleaf\nMotoru" as thymeleaf
participant "I18n\nÇözümleyici" 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: "Ana Sayfa"
i18n --> thymeleaf: "Ana Sayfa"
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: "Ev"
i18n --> thymeleaf: "Ev"
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

JBake’te yerel ayar yapılandırması

Dosyanızda`jbake.properties`, varsayılan locale’yi ayarlayın :

# Locale par défaut
thymeleaf.locale=fr

# Encodage
template.encoding=UTF-8

Makalelerin I18n’i: Klasör bazlı organizasyon

akış diyagramı (Flow) - oluşturma

@startuml
start

:Lire jbake.properties;
:Charger configuration i18n;

partition "Her AsciiDoc dosyası için" {
  :Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
  :Parser contenu AsciiDoc;
  :Stocker en mémoire avec langue;
}

partition "Her şablon için" {
  :Charger template Thymeleaf;
  :Identifier langue cible;
  :Charger messages_{lang}.properties;

  partition "İçerik filtreleme" {
    :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

Klasör yapısı

Dosya adlarında sonek kullanmak yerine, daha fazla açıklık ve bakım kolaylığı sağlayan klasör tabanlı bir organizasyonu tercih ettim:

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

Bu yaklaşımın avantajları

Bu yapı birden fazla avantaja sahiptir :

  • net ayrım: Her dilin kendi alanı var.

  • Esnek adlandırma: dosyaların dila göre farklı isimler olabilir

  • Ölçeklenebilirlik: yeni bir dil eklemek kolay

  • Doğal organizasyonJBake’in zaman mantığını takip eder

Makalelerin meta verileri

Her makale, çeviriler arasında bağlantı kurmak için meta veriler içermelidir. Bir örnek şöyle:

Fransızca sürüm(2025/fr/internationalisation-jbake.adoc) :

= Internationalisation d'un site statique JBake
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: tr
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide pour mettre en place l'i18n avec JBake

İngilizce sürüm(2025/en/jbake-internationalization.adoc) :

= Internationalizing a JBake Static Site
:jbake-type: post
:jbake-status: published
:jbake-date: 2025-10-20
:jbake-lang: tr
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide to implement i18n with JBake

Not: Özellik`:jbake-article-id:`kritiktir: bir makalenin farklı çevirileri arasında bağlantı kurar.

URL yapılandırması

içinde`jbake.properties`, URL desenini dil içerecek şekilde yapılandırın:

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

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

Bu şu tür URL’leri oluşturacaktır:

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

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

Çoklu dil görüntüleme şablonları

Dil seçici makale şablonu

Şablon oluşturun`post.html`Kullanılabilir çevirileri gösteren :

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

Dil tarafından filtrelenmiş indeks

Her dil için dizin şablonları oluşturun :

index.html(Fransızca indeks) :

<!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(İngiliz indeksi) :

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

Diller arasında gezinme

Global dil seçici

Akış diyagramı - Kullanıcı okuması

@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

Ana şablonunuza bir dil seçici ekleyin :

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

Seçici için CSS stil

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

Dil bazlı RSS akışı

Dilara ayrılmış RSS akışları almak için, ayrı şablonlar oluşturun :

feed.xml( Français akım) :

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

İyi uygulamalar ve ipuçları

1. Makale kimlik tutarlılığı

Emin olun ki`:jbake-article-id:`Bütün bir makalenin çevirileri için aynıdır. Tutarlı bir format kullanın:

  • Küresellik için İngilizce tanımlayıcıları tercih edin.

  • Kelimeleri ayırmak için tiret kullanın.

  • Özel karakterlerden kaçının

2. Tutarlı tarihler

Bir makalenin tüm çevirileri aynı yayın tarihine sahip olmalıdır.:jbake-date:). Bu, sıralama ve kronolojik görüntülemeyi kolaylaştırır.

3. Çok dilli etiketler

Etiketler için iki seçeneğiniz var:

Seçenek 1: İngilizce evrensel etiketler

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

Seçenek 2: Eşlemeyle çevrilmiş etiketler

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

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

4. Çevirilmemiş makalelerin yönetimi

Tüm makaleleri çevirmek zorunlu değildir. Bir makale sadece bir dilde mevcutsa, diğer dildeki listelerde görünmez olur.

5. Çok dilli site haritası

Tüm dilleri içeren bir site haritası oluşturun :

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

Sonuç

JBake sitesinin Thymeleaf ile uluslararasılaştırılması, sağlam ve sürdürülebilir bir yaklaşımdır. Şablondan (mesaj dosyaları üzerinden) ve içerikten (klasör organizasyonu üzerinden) i18n’i ayırarak, kolayca gelişebilecek esnek bir sistem elde edersiniz.

Aklınızda bulundurmanız gereken önemli noktalar:

  • Thymeleaf mesaj dosyalarıkullanıcı arayüzü için

  • Dossiye göre düzenleme(yıl/dil) makaleler için

  • ürün kimlikleripour lier les traductions → çevirileri bağlamak

  • özel şablonlarher dil için

  • Açık URL’lerdil kodunu içeren

Dağıtım diyagramı

@startuml
node "Geliştirme makinesi" {
  artifact "Kaynaklar" {
    folder "memnun/"
    folder "şablonlar/"
    folder "assets/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "Derleme sunucusu\n(CI/CD)" {
  component "GitHub Actions\nveya GitLab CI" as ci

  jbake --> ci : push
}

cloud "CDN / Barındırma" {
  node "Statik Web Sunucusu" {
    artifact "Oluşturulan Site" {
      folder "/fr/blog/"
      folder "/en/blog/"
      folder "/assets/"
    }
  }
}

ci --> "Oluşturulan Site" : déploiement

actor "Okuyucular" as users

users --> "Statik Web Sunucusu" : HTTPS
@enduml

Bu mimari, sadece iki dil ile başlangıç yapmanızı ve büyük bir refaktörleme yapmadan diğer dilleri eklemenizi sağlar. Tüm şey tamamen statik ve performant kalır, JBake felsefesine sadık kalır.

Çok dilli geliştirme iyi geçsin! 🌍

İlgili Makaleler