Увод

Internacionalizacija (i18n) statičkog sajta može se činiti složeno na prvi pogled, ali JBake u kombinaciji sa Thymeleaf-om nudi elegantna rešenja za kreiranje višezbojnog sajta. U ovom članaku ću vam pokazati kako sam postavio i18n na svom blogu, pokrjujući tako templating i upravljanje člancima na više jezika.

Дијаграм случајева коришћења (Use Case)

@startuml
left to right direction
skinparam packageStyle rectangle

actor "Аутор" as author
actor "Čitač FR" as reader_fr
actor "Čitač EN" as reader_en

rectangle "сајт JBake вишејезични" {
  usecase "Напиши članak FR" as UC1
  usecase "Написати članak EN" as UC2
  usecase "Konfiguriši i18n poruke" as UC3
  usecase "Генериши сајт" as UC4
  usecase "Pročitaj članak FR" as UC5
  usecase "Прочитај чланак EN" as UC6
  usecase "Promeniti jezik" as UC7
  usecase "Pogledajte dostupne prevodi" 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

Arhitektura internacionalizacije

Naš pristup se zasniva na dva pilija :

  1. I18n templatinga: korišćenje datoteka poruka Thymeleaf za elemente interfejsa

  2. I18n sadržajaorganizacija artikala po jeziku u posebnoj strukturi direktorijuma

Strukturalni dijagram (Organizacija fajlova)

@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

Zašto ovaj pristup?

Ovo razdvojenje omogućava:

  • Одржајте кохерентност у интерфесу без обзира на језик

  • Samostalno upravljati sadržajem i prevodima članaka

  • Olaksati dodavanje novih jezika bez veće refaktorizacije

  • Omogućiti članke dostupne samo na nekim jezicima

Dijagram komponenti

@startuml
skinparam componentStyle rectangle

package "Izvori" {
  folder "sadržaj/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 "шаблони/" {
    [post.html]
    [index.html]
    [index_en.html]
  }

  folder "poruke/" {
    [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 "Generisan sajt" {
  folder "fr/" {
    folder "блог/2025/" as blog_fr {
      [article1.html]
      [article2.html]
    }
  }

  folder "en/" {
    folder "блог/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 templatanja pomoću Thymeleaf

Структура фајлова порука

Prvi korak je da se kreiraju fajlovi svojstva za svaki podržani jezik:

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

Sadržaj datoteka poruka

Ovo je primer datoteke`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...

И његов еквивалент на engleskom`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...

Upotreba u šablonima

У вашим Thymeleaf шаблонима, користите синтаксу`#{}`Za pristup porukama :

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

Дијаграм секвенце - Решење i18n

@startuml
actor Auteur
participant JBake
participant "AsciiDoc\nParser" as parser
participant "Thymeleaf
Motor" as thymeleaf
participant "I18n\nРешавач" 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: "Почетна"
i18n --> thymeleaf: "Почетна"
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: "Početna"
i18n --> thymeleaf: "Početna"
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

U vašoj datoteci`jbake.properties`, postavite podrazumevanu lokalu :

# Locale par défaut
thymeleaf.locale=fr

# Encodage
template.encoding=UTF-8

I18n artikala : organizacija po direktorijumima

Dijagram toka (Flow) - Generisanje

@startuml
start

:Lire jbake.properties;
:Charger configuration i18n;

partition "За svaku AsciiDoc datoteku" {
  :Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
  :Parser contenu AsciiDoc;
  :Stocker en mémoire avec langue;
}

partition "Za svaki šablon" {
  :Charger template Thymeleaf;
  :Identifier langue cible;
  :Charger messages_{lang}.properties;

  partition "Filtriranje sadržaja" {
    :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

Структура фасцикла

Umesto da koristim suffikse u nazivu datoteke, odlučio sam za organizaciju po folderima koja pruža bolju jasnoću i održivost:

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

Предности овог приступа

Ova struktura ima više prednosti:

  • jasno odvajanje: svaki jezik ima svoj prostor

  • fleksibilno imeovanje: datoteke mogu imati različite nazive u zavisnosti od jezika

  • Skalabilnost: lak je dodati novi jezik

  • Природна организација: prati vremensku logiku JBake

Metapodaci članaka

Svaki članak mora sadržavati metapodatke da bi se omogućilo povezivanje između prevoda. Ovo je primer:

Francuska verzija(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

Engleska verzija(empty)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

NAPOMENA: Atribut`:jbake-article-id:`је битно : он омогућава везивање различитих превода истог чланка.

Конфигурација URL-ова

U`jbake.properties`, podesite uzorak URL-a da uključuje jezik :

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

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

Ovo će generisati URL-ove tipa :

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

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

Šabloni za višejezični prikaz

Шаблон статье са селектором језика

Направи шаблон`post.html`ко приказује доступне преводе :

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

Filtrovan indeks po jeziku

Kreirajte šablone indeksa za svaki jezik :

index.html(indeks francuski) :

<!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(engleski indeks) :

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

Навигација између језика

Globalni selektor jezika

Dijagram toka - čitanje korisnika

@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

Dodajte selektor jezika u vaš glavni šablon:

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

CSS stil za selektor

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

RSS kanal po jeziku

Da biste imali RSS tokove odvojeni po jeziku, kreirajte zasebne šablone :

feed.xml(francuski tok) :

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

Dobre prakse i saveti

1. Konzistentnost identifikatora artikla

Uverite se da`:jbake-article-id:`je identično za sve prevede od istog članka. Koristite konzistentan format :

  • Preferirajte engleske identifikatore radi univerzalnosti.

  • Koristić crtice za razdvajanje reči.

  • Izbegavajte specijalne karaktere

2. Konsekventni datumi

Sve prevodi jednog članka moraju imati isti datum objave (:jbake-date:). Ovo olakšava sortiranje i kronološki prikaz.

3. Višejezični tagovi

Za tagove, imate dve opcije:

Опција 1: Универзални тагови на английском

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

Опција 2: тагове преведени са мапингом

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

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

4. Upravljanje neprevedenim članacima

Nije obavezno prevedati sve članke. Ako članak postoji samo na jednom jeziku, jednostavno neće se pojaviti u popisima drugog jezika.

5. Višejezična sitemap

Generišite jedan sitemap koji uključuje sve jezike:

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

Zaključak

Internacionalizacija JBake sajta pomoću Thymeleaf-a je jak i održiv pristup. Odvajanjem i18n od templatinga (putem datoteka poruka) i i18n od sadržaja (putem organizacije u fasciklama), dobijate fleksibilni sistem koji se lako razvija.

Клучне тачке за запамећавање:

  • Datoteke poruka Thymeleafза кориснички интерфејс

  • Organizacija po fasciklima(godina/jezik) za članke

  • Identifikatori artiklaповезати преводи

  • Посвећени шаблониza svaki jezik

  • Eksplicitni URL-oviuključujući kod jezika

Dijagram raspoređivanja

@startuml
node "Mašina za razvoj" {
  artifact "Izvor" {
    folder "srećan/"
    folder "šabloni/"
    folder "sredstva/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "Server za izgradnju
(CI/CD)" {
  component "GitHub Actions
ili GitLab CI" as ci

  jbake --> ci : push
}

cloud "CDN / hosting" {
  node "Statički web server" {
    artifact "Генерисан сајт" {
      folder "/blog/"
      folder "/en/blog/"
      folder "/assets/"
    }
  }
}

ci --> "Генерисан сајт" : déploiement

actor "Čitaoci" as users

users --> "Statički web server" : HTTPS
@enduml

Ova arhitektura vam omogućava da pođete jednostavno sa dva jezika i da dodate još jedan bez velike refaktorisanja. Sve ostaje potpuno statičko i performantno, verno filozofiji JBake.

Dobar višijezični razvoj! 🌍

Повезани чланци