بینالمللیسازی سایت استاتیک JBake با Thymeleaf
منتشر شده در 20 October 2025
معرفی
بینالمللیسازی (i18n) یک وبسایت ایستا ممکن است در ابتدا به نظر پیچیده بیاید، اما JBake به همراه Thymeleaf راهحلهای شایست برای ساخت یک وبسایت چندزبانه فراهم میکند. در این مقاله، به شما نشان میدهم که چگونه i18n را در وبلاگ خود پیادهسازی کردم، شامل هم قالببندی و هم مدیریت مقالهها در چندین زبان.
نمودار مورد استفاده (Use Case)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "نویسنده" as author
actor "خواننده FR" as reader_fr
actor "خواننده انگلیسی" as reader_en
rectangle "سایت JBake چند زبانه" {
usecase "نوشتن مقالهٔ FR" as UC1
usecase "نوشتن مقاله به انگلیسی" as UC2
usecase "پیامهای i18n را تنظیم کنید" as UC3
usecase "ایجاد سایت" as UC4
usecase "مقاله FR را بخوان" as UC5
usecase "خواندن مقاله EN" as UC6
usecase "تغییر زبان" as UC7
usecase "ترجمههای موجود را ببینید" 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
معماری بینالمللیسازی
رویکرد ما بر دو پایه استوار است:
-
بینالمللیسازی قالببندی: استفاده از فایلهای پیام Thymeleaf برای عناصر رابط کاربری
-
i18n محتواسازماندهی مقالات بر اساس زبان در یک ساختار پوشه اختصاصی
نمودار ساختار (سازمان فایلها)
@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
چرا این رویکرد؟
این جداسازی به ما اجازه میدهد که:
-
حفظ سازگاری در رابط، صرفنظر از زبان
-
محتوای مقالات و ترجمات آنها را بهصورت مستقل مدیریت کنید
-
تسهیل کردن افزودن زبانهای جدید بدون بازنگری بزرگ
-
اجازه دسترسی به مقالات موجود فقط به برخی زبانها
نمودار مؤلفهها
@startuml
skinparam componentStyle rectangle
package "منابع" {
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 "الگو/" {
[post.html]
[index.html]
[index_en.html]
}
folder "پیامها/" {
[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 "سایت تولید شده" {
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 قالببندی با Thymeleaf
ساختار فایلهای پیام
مرحله اول ایجاد فایلهای ویژگی برای هر زبان پشتیبانی شده است:
src/jbake/templates/
├── messages.properties # Fallback par défaut
├── messages_fr.properties # Français
├── messages_en.properties # Anglais
└── messages_de.properties # Allemand
محتویات فایلهای پیام
این یک مثال فایل است`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...
و معادل انگلیسی آن`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...
استفاده در الگوهای
در قالبهای Thymeleaf شما، از این سینتکس استفاده کنید`#{}`برای دسترسی به پیامها :
<!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
موتور" as thymeleaf
participant "I18n\nحلال" as i18n
database "پیام_*.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: "خانه"
i18n --> thymeleaf: "خانه"
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
در فایل شما`jbake.properties`, Locale پیشفرض را تنظیم کنید :
# Locale par défaut
thymeleaf.locale=fr
# Encodage
template.encoding=UTF-8
بینالمللیسازی مقالات : سازماندهی بر اساس پوشهها
نمودار جریان (Flow) - تولید
@startuml
start
:Lire jbake.properties;
:Charger configuration i18n;
partition "برای هر فایل AsciiDoc" {
:Lire métadonnées\n(:jbake-lang:, :jbake-article-id:);
:Parser contenu AsciiDoc;
:Stocker en mémoire avec langue;
}
partition "برای هر قالب" {
:Charger template Thymeleaf;
:Identifier langue cible;
:Charger messages_{lang}.properties;
partition "فیلتر کردن محتوا" {
: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
ساختار پوشهها
به جای استفاده از پسوندها در نامهای فایلها، من ترجیح دادم بهجای آن از سازماندهی پوشهای استفاده کنم که شفافیت و قابلیت نگهداری بیشتری ارائه میدهد :
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
مزایای این رویکرد
این ساختار مزایای متعددی دارد :
-
جدایی واضح: هر زبان فضای خود را دارد
-
نامگذاری انعطافپذیر: فایلها میتوانند بسته به زبان نامهای متفاوتی داشته باشند
-
مقیاسپذیری: آسان است اضافه کردن یک زبان جدید
-
سازمان طبیعیمنطق زمانی JBake را دنبال میکند
میتادیتا مقالات
هر مقاله باید حاوی متاداده باشد تا امکان پیوند بین ترجمهها فراهم شود. این یک مثال است :
نسخه فرانسوی(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
نسخه انگلیسی(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
نوت: خصوصیت`:jbake-article-id:`ضروری است: این امکان را میدهد تا ترجمههای مختلف یک مقاله را به هم متصل سازیم.
پیکربندی آدرسها
در`jbake.properties`, الگوی آدرس URL را برای شامل کردن زبان تنظیم کنید:
# Pattern d'URL avec langue
post.permalink.pattern=:lang/blog/:year/:name.html
# Langue par défaut
site.default.lang=fr
این آدرسهای URL از این نوع را ایجاد خواهد کرد :
-
/fr/blog/2025/internationalisation-jbake.html -
/en/blog/2025/jbake-internationalization.html
الگوهای نمایش چندزبانه
قالب مقاله با انتخابکننده زبان
یک قالب ایجاد کنید`post.html`که traductions disponibles را نمایش میدهد :
<!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>
فهرست فیلتر شده بر اساس زبان
الگویهای index برای هر زبان ایجاد کنید :
index.html(فهرست فرانسوی) :
<!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(اندیس انگلیسی) :
<!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>
ناوبری بین زبانها
انتخابگر زبان جهانی
دیاگرام جریان - خواندن کاربر
@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
یک انتخابکننده زبان را در قالب اصلی خود اضافه کنید:
<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 برای انتخابگر
.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 به زبان
برای داشتن فیدهای RSS جداگانه بر اساس زبان، قالبهای جداگانه بسازید:
feed.xml(فلو فرانسوی) :
<?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>
نکات و بهترین روشها
1. همگونی شناسههای مقاله
اطمینان حاصل کنید که`:jbake-article-id:`برای تمام ترجمهای یک مقاله یکسان است. از یک فرمت منسق استفاده کنید:
-
شناسههای انگلیسی را برای جهانی بودن ترجیح دهید
-
برای جداسازی کلمات از tirets استفاده کنید
-
کاراکترهای خاص را اجتناب کنید
2. تاریخهای سازگار
تمام ترجمات یک مقاله باید تاریخ انتشار یکسانی داشته باشند.:jbake-date:). این مرتبسازی و نمایش به ترتیب زمانی را آسان میسازد.
3. برچسبهای چندزبانه
برای برچسبها، دو گزینه دارید :
گزینه 1: برچسبهای جهانی به انگلیسی
:jbake-tags: java, spring-boot, microservices
Option 2 : برچسبها با نگاشت ترجمه شده
# Version française
:jbake-tags: java, spring-boot, microservices
# Version anglaise
:jbake-tags: java, spring-boot, microservices
4. مدیریت مقالههای غیرمترجم
ترجمه تمام مقالات الزامی نیست. اگر یک مقاله فقط در یک زبان موجود باشد، به سادگی در فهرستهای زبان دیگر نمایش داده نخواهد شد.
5. نقشه سایت چندزبانه
یک سایت&Mprehensive؟
<?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>
نتیجه
بینالمللیسازی یک سایت JBake با Thymeleaf یک رویکرد مقاوم و قابل نگهداری است. با جداسازی i18n از تمپلتینگ (از طریق فایلهای پیغامها) و i18n از محتوا (از طریق سازماندهی در پوشهها)، شما یک سیستم انعطافپذیر بهدست میآورید که میتواند بهراحتی توسعه یابد.
نکات کلیدی که باید به خاطر بسپارید:
-
فایلهای پیام Thymeleafبرای رابط کاربری
-
سازگاری بر اساس پوشهها(سال/زبان) برای مقالات
-
شناسههای مقالهبرای پیوند traductions
-
قالبهای اختصاصیبرای هر زبان
-
URLهای صریحشامل کد زبان
نمودار استقرار
@startuml
node "ماشین توسعه" {
artifact "منابع" {
folder "محتوا/"
folder "templates/"
folder "assets/"
}
component "JBake CLI" as jbake
Sources --> jbake : jbake -b
}
node "سرور ساخت
(CI/CD)" {
component "GitHub Actions
یا GitLab CI" as ci
jbake --> ci : push
}
cloud "CDN / میزبانی" {
node "سرور وب استاتیک" {
artifact "سایت تولیدشده" {
folder "/fr/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "سایت تولیدشده" : déploiement
actor "خوانندگان" as users
users --> "سرور وب استاتیک" : HTTPS
@enduml
این معماری به شما اجازه میدهد تا بهراحتی با دو زبان شروع کنید و زبانهای دیگر را بدون نیاز به بازنگری بزرگ اضافه کنید. کل این امر بهطور کامل ایستا و کارآمد است و وفادار به فلسفهٔ JBake میماند.
توسعه چند زبانه خوب! 🌍