التدويل لموقع ثابت JBake باستخدام Thymeleaf
نُشر في 20 October 2025
مقدمة
يمكن أن يبدو تطبيق الت internazionale (i18n) لموقع ثابت معقدًا في البداية، لكن JBake جنبًا إلى جنب مع Thymeleaf يوفران حلولًا أنيقة لإنشاء موقع متعدد اللغات. في هذا المقال، سأريكم كيف قمت بإعداد i18n على مدونتي، مع تغطية كلٍ من التemplat وإدارة المقالات بلغات متعددة.
رسم بياني لحالة الاستخدام (حالة استخدام)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "المؤلف" as author
actor "قارئ FR" as reader_fr
actor "قارئ EN" as reader_en
rectangle "موقع JBake متعدد اللغات" {
usecase "كتابة مقال FR" as UC1
usecase "كتابة مقال EN" 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 لعناصر الواجهة
-
التدويل للمحتوىتنظيم المقالات حسب اللغة داخل بنية مجلدات مخصصة
مخطط الهيكل (تنظيم الملفات)
@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 محرك" {
[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
التدويل للقالب باستخدام 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\nEngine" as thymeleaf
participant "I18n\nResolver" as i18n
database "messages_*.properties" as messages
Auteur -> JBake: jbake -b
activate JBake
JBake -> parser: اقرأ article.fr.adoc
activate parser
parser --> JBake: محتوى + بيانات وصفية\n(lang=fr, article-id=xyz)
deactivate parser
JBake -> parser: اقرأ article.en.adoc
activate parser
parser --> JBake: محتوى + بيانات\n(lang=en, article-id=xyz)
deactivate parser
JBake -> thymeleaf: إنشاء صفحة FR
activate thymeleaf
thymeleaf -> i18n: حل #{nav.home}
activate i18n
i18n -> messages: اقرأ messages_fr.properties
messages --> i18n: "الصفحة الرئيسية"
i18n --> thymeleaf: "الصفحة الرئيسية"
deactivate i18n
thymeleaf -> JBake: ابحث عن الترجمات\n(article-id=xyz, lang!=fr)
JBake --> thymeleaf: article.en.html وُجد
thymeleaf --> JBake: /fr/blog/article.html
deactivate thymeleaf
JBake -> thymeleaf: إنشاء صفحة EN
activate thymeleaf
thymeleaf -> i18n: حل #{nav.home}
activate i18n
i18n -> messages: اقرأ messages_en.properties
messages --> i18n: "Home"
i18n --> thymeleaf: "Home"
deactivate i18n
thymeleaf -> JBake: بحث الترجمات\n(article-id=xyz, lang!=en)
JBake --> thymeleaf: article.fr.html موجود
thymeleaf --> JBake: /en/blog/article.html
deactivate thymeleaf
JBake --> Auteur: موقع تم إنشاؤه
deactivate JBake
@enduml
إعدادات الإقليم في JBake
في ملفك`jbake.properties`, اضبط الإعدادات الافتراضية :
# Locale par défaut
thymeleaf.locale=fr
# Encodage
template.encoding=UTF-8
I18n المقالات : التنظيم حسب المجلدات
مخطط التدفق (Flow) - توليد
@startuml
start
:اقرأ jbake.properties;
:تحميل التكوين i18n;
partition "لكل ملف AsciiDoc" {
:قراءة البيانات التعريفية\n(:jbake-lang:, :jbake-article-id:);
:تحليل المحتوى AsciiDoc;
:احفظ في الذاكرة مع اللغة;
}
partition "لكل قالب" {
:تحميل القالب Thymeleaf;
:Identifier langue cible;
:تحميل messages_{lang}.properties;
partition "تصفية المحتوى" {
:تصفية المقالات حسب اللغة;
:تجميع الترجمات\nحسب معرّف المقالة;
}
:تطبيق القالب مع i18n;
:إنشاء HTML في /{lang}/;
}
:نسخ الأصول الثابتة;
:إنشاء خريطة موقع متعددة اللغات;
:إنشاء تدفق RSS لكل لغة;
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: ar
: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: ar
:jbake-article-id: jbake-i18n-thymeleaf
:jbake-tags: jbake, thymeleaf, i18n
:jbake-description: Guide to implement i18n with JBake
ملاحظة: السمة`:jbake-article-id:`هو حاسم: يتيح ربط الترجمات المختلفة لنفس المقال.
إعداد عناوين URL
في`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`الذي يعرض الترجمات المتاحة :
<!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.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
:المستخدم يدخل إلى الموقع;
if (Langue préférée ?) then (FR)
:عرض /index.html;
:قائمة المقالات FR;
else (EN)
:عرض /en/index.html;
:قائمة المقالات EN;
endif
:المستخدم يضغط على المقال;
:عرض article\nمع البيانات الوصفية;
if (Traductions disponibles ?) then (oui)
:عرض محدد\n للترجمات;
if (Changement de langue ?) then (oui)
:توجيه إلى\nالترجمة;
:عرض article\nباللغة الجديدة;
else (non)
:استمر في القراءة;
endif
else (non)
:تابع القراءة;
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>
ممارسات جيدة ونصائح
الاتساق في معرّفات المقال
تأكد من`:jbake-article-id:`هو نفسه لجميع ترجمات المقال الواحد. استخدم تنسيقًا موحدًا :
-
يفضل استخدام المعرّفات بالإنجليزية من أجل العالمية
-
استخدم الشرطة لفصل الكلمات
-
تجنب الأحرف الخاصة
2. التواريخ المتسقة
يجب أن تكون جميع الترجمات لمقال واحد بنفس تاريخ النشر`:jbake-date:`). هذا يسهّل الفرز والعرض الزمني
3. العلامات متعددة اللغات
بالنسبة للوسوم، لديك خياران :
الخيار 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. إدارة المقالات غير المترجمة
ليس من الضروري ترجمة جميع المقالات. إذا كان المقال موجودًا فقط بلغة واحدة، فلن يظهر ببساطة في قوائم اللغة الأخرى.
خريطة موقع متعددة اللغات
أنشئ خريطة موقع تشمل جميع اللغات :
<?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 عن المحتوى (تنظيم المجلدات)، تحصل على نظام مرن يمكن أن يتطور بسهولة.
النقاط الرئيسية التي يجب تذكرها :
-
ملفات رسائل ثيمليفللواجهة المستخدم
-
تنظيم حسب المجلدات(سنة/لغة) للمقالات
-
معرفات المقاللربط الترجمات
-
قوالب مخصصةلكل لغة
-
روابط صريحةبما في ذلك رمز اللغة
مخطط النشر
@startuml
node "آلة تطوير" {
artifact "مصادر" {
folder "المحتوى/"
folder "القوالب/"
folder "الأصول/"
}
component "JBake CLI" as jbake
Sources --> jbake : جبيك -ب
}
node "خادم البناء\n(CI/CD)" {
component "GitHub الإجراءات\nأو GitLab CI" as ci
jbake --> ci : دفع
}
cloud "CDN / استضافة" {
node "خادم ويب ثابت" {
artifact "موقع تم إنشاؤه" {
folder "/fr/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "موقع تم إنشاؤه" : déploiement
actor "قارئون" as users
users --> "خادم ويب ثابت" : HTTPS
@enduml
تسمح لك هذه الهندسة بالبدء ببساطة مع لغتين وإضافة لغات أخرى دون الحاجة إلى إعادة هيكلة رئيسية. ويظل كل شيء ثابتًا بالكامل وأداءً جيدًا، مخلصًا لفلسفة JBake.
تطوير متعدد اللغات جيد! 🌍