معرفی

بین‌المللی‌سازی (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

معماری بین‌المللی‌سازی

رویکرد ما بر دو پایه استوار است:

  1. بین‌المللی‌سازی قالب‌بندی: استفاده از فایل‌های پیام Thymeleaf برای عناصر رابط کاربری

  2. 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 می‌ماند.

توسعه چند زبانه خوب! 🌍

مقالات مرتبط