타임리프를 사용한 JBake 정적 사이트의 국제화
게시: 20 October 2025
소개
정적 웹사이트의 국제화(i18n)는 처음엔 복잡해 보일 수 있지만, JBake와 Thymeleaf를 결합하면 다국어 사이트를 구축하기 위한 우아한 솔루션을 제공합니다. 이 글에서는 블로그에 i18n을 어떻게 구현했는지 보여드리겠습니다. 이때 템플릿과 다국어 게시글 관리를 모두 다룰 것입니다.
사용 사례 다이어그램 (Use Case)
@startuml
left to right direction
skinparam packageStyle rectangle
actor "작가" as author
actor "프랑스어 리더" 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
국제화 아키텍처
우리 접근 방식은 두 가지 기둥을 기반으로 합니다.
-
템플릿의 i18n: 인터페이스 요소에 대한 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 "영어/" as content_en {
[article1.adoc] as article1_en
[article2.adoc] as article2_en
}
}
folder "템플릿/" {
[post.html]
[index.html]
[index_en.html]
}
folder "(empty)" {
[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 "영어/" {
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
파서" as parser
participant "타임리프
엔진" as thymeleaf
participant "I18n
해결사" 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: "집"
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) - 생성
@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의 시간적 논리를 따릅니다
기사의 메타데이터
각 문서는 번역 간 연결을 허용하기 위해 메타데이터를 포함해야 합니다. 다음은 예시 :
프랑스어 버전</think> (No content to translate)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>
언어 간 내비게이션
글로벌 언어 선택기
흐름도 - 사용자 보기
@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:`는 같은 기사의 모든 번역에 대해 동일합니다. 일관된 형식을 사용하세요:
-
영문 식별자를 선호하여 범용성을 높이세요
-
단어를 구분하기 위해 하이픈을 사용하세요.
-
특수 문자를 피하세요
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과 함께 국제화하는 것은 강력한 유지 관리 가능한 접근 방식입니다. 메시지 파일을 통해 템플릿의 i18n을 분리하고 폴더 조직을 통해 콘텐츠의 i18n을 분리함으로써, 쉽게 발전할 수 있는 유연한 시스템을 얻을 수 있습니다.
기억해야 할 핵심 사항:
-
Thymeleaf 메시지 파일사용자 인터페이스를 위해
-
폴더별 조직(연도/언어) 기사용
-
아이템 식별자번역들을 연결하기 위해
-
전용 템플릿각 언어마다
-
명시적 URL언어 코드를 포함하여
배포도
@startuml
node "개발 기계" {
artifact "출처" {
folder "만족한"
folder "템플릿/"
folder "자산/"
}
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 "/ko/blog/"
folder "/en/blog/"
folder "/assets/"
}
}
}
ci --> "생성된 사이트" : déploiement
actor "독자들" as users
users --> "정적 웹 서버" : HTTPS
@enduml
이 아키텍처를 사용하면 두 언어로 시작하기가 간편하고 추가 언어를 추가할 수 있으며 큰 리팩터링이 필요하지 않습니다. 모든 것이 완전히 정적이며 고성능을 유지하며 JBake 철학을 따릅니다.
다국어 개발 잘하세요! 🌍
관련 기사
31 May 2026
14 May 2026