Internationalizacija statičnog JBake sajta uz Thymeleaf
Објављено 20 October 2025
Увод
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 :
-
I18n templatinga: korišćenje datoteka poruka Thymeleaf za elemente interfejsa
-
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! 🌍