مقدمة

يمكن أن يبدو تعريب (i18n) لموقع ثابت معقدًا في البداية، لكن JBake المدمج مع Thymeleaf يقدّم حلولًا أنيقة لإنشاء موقع متعدد اللغات. في هذه المقالة، سأريكم كيف قمت بإعداد التعريب (i18n) على مدونتي، مع تغطية كل من القالب وإدارة المقالات بعدة لغات.

مخطط حالة الاستخدام (Use Case)

Diagram

معمارية التدويل

نهجنا يرتكز على عمودين :

  1. الدّوليّة (i18n) للقالب: استخدام ملفات رسائل Thymeleaf للعناصر الواجهة

  2. i18n للمحتوىتنظيم المقالات حسب اللغة في هيكل مجلد مخصص

مخطط البنية (تنظيم الملفات)

Diagram

لماذا هذا النهج؟

هذا الفصل يسمح ب:

  • الحفاظ على التماسك في الواجهة بغض النظر عن اللغة

  • إدارة المحتوى وترجمات المقالات بشكل مستقل

  • تسهيل إضافة لغات جديدة دون الحاجة إلى إعادة هيكلة رئيسية

  • السماح بالمقالات المتوفرة فقط في بعض اللغات

مخطط المكونات

Diagram

I18n للتemplating مع 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

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 5) ]

@startuml
actor Auteur
participant JBake
participant "AsciiDoc\nمحلل" as parser
participant "Thymeleaf
^^^^^
 Syntax Error? (Assumed diagram type: sequence)

@startuml
actor Auteur
participant JBake
participant "AsciiDoc\nمحلل" as parser
participant "Thymeleaf
محرك" as thymeleaf
participant "I18n
Resolver" 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 par défaut
thymeleaf.locale=fr

# Encodage
template.encoding=UTF-8

I18n للمقالات : التنظيم حسب المجلدات

مخطط التدفق (Flow) - التوليد

Diagram

هيكل المجلدات

بدلاً من استخدام لاحقات في أسماء الملفات، اخترت تنظيمًا بالمجلدات يوفر وضوحًا أكبر وسهولة صيانة :

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:`إنه حاسم : يسمح بربط الترجمات المختلفة لنفس المقالة.

تكوين عناوين 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>

التنقل بين اللغات

محدد اللغة العالمي

مخطط تدفق - قراءة المستخدم

</think>

مخطط تدفق - قراءة المستخدم

Diagram

أضف محدد لغة في القالب الرئيسي:

<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:`هو نفسه لجميع ترجمات نفس المقال. استخدم تنسيقًا موحدًا :

  • اِفْضُلُوا المعرفات باللغة الإنجليزية للعالمية

  • استخدم شرطات لفصل الكلمات

  • تجنب الأحرف الخاصة

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. إدارة المقالات غير المترجمة

ليس من الضروري ترجمة جميع المقالات. إذا كان المقال موجودًا فقط بلغة واحدة، فلن يظهر ببساطة في قوائم اللغة الأخرى.

5. خريطة موقع متعددة اللغات

أنشئ خريطة الموقع التي تشمل جميع اللغات :

<?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 هو نهج قوي وقابل للصيانة. من خلال فصل التدويل عن القالب (عبر ملفات الرسائل) وعن المحتوى (عبر تنظيم المجلدات)، تحصل على نظام مرن يمكن تطويره بسهولة.

النقاط الأساسية التي يجب تذكرها :

  • ملفات رسائل Thymeleafلواجهة المستخدم

  • تنظيم حسب المجلدات(سنة/لغة) للمقالات

  • معرّفات المقاللربط الترجمات

  • **قالب مخصص is not correct; we need "قوالب مخصصة". Ensure correct translation: "قوالب مخصصة". Output exactly that.

</think>

قالب مخصص is wrong; let’s correct: القوالب المخصصة? Actually "templates dedicated" = "قوالب مخصصة". We’ll output that.

</think>

قوالب مخصصة**لكل لغة

  • روابط صريحةبما في ذلك رمز اللغة

مخطط النشر

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 14) ]

@startuml
node "آلة التطوير" {
  artifact "مصادر" {
    folder "المحتوى/"
    folder "قوالب/"
    folder "الأصول/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "خادم البناء
^^^^^
 Syntax Error? (Assumed diagram type: component)

@startuml
node "آلة التطوير" {
  artifact "مصادر" {
    folder "المحتوى/"
    folder "قوالب/"
    folder "الأصول/"
  }

  component "JBake CLI" as jbake

  Sources --> jbake : jbake -b
}

node "خادم البناء
(CI/CD)" {
  component "GitHub Actions\nأو 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.

تطوير متعدد اللغات جيد! 🌍

Articles connexes