Spring Boot i18n: 국제화 설정 튜토리얼
MessageSource를 구성하고, 로케일별 properties 파일을 만들며, 로케일을 결정하고, 다국어 Thymeleaf 템플릿을 렌더링한 다음 AI로 번역을 자동화해요.
종속성 추가
Spring Boot Starter Web에는 MessageSource 자동 구성이 기본으로 포함되어 있어요. 서버에서 렌더링하는 i18n 템플릿을 위해 Thymeleaf를 추가하고, 현지화된 오류 메시지를 위해 유효성 검사 스타터를 추가해요.
<!-- pom.xml — Spring Boot Starter Web includes MessageSource auto-config -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf for server-side rendered templates with i18n -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Validation (for localized error messages) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>MessageSource 및 LocaleResolver 구성
Spring의 MessageSource는 basename 규칙에 따라 .properties 파일에서 번역을 불러와요: messages.properties(기본값), messages_de.properties(독일어), messages_ja.properties(일본어). 요청마다 사용할 로케일을 결정하도록 LocaleResolver를 구성해요.
번역 파일
# src/main/resources/messages.properties (default / English)
nav.home=Home
nav.about=About
nav.settings=Settings
greeting=Hello, {0}!
cart.itemCount={0,choice,0#No items|1#1 item|1<{0,number} items}
error.notFound=Page not found
error.serverError=Something went wrong. Please try again.MessageSource 구성
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class I18nConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource source =
new ReloadableResourceBundleMessageSource();
source.setBasename("classpath:messages");
source.setDefaultEncoding("UTF-8");
source.setCacheSeconds(3600); // reload interval in dev
return source;
}
// Wire MessageSource into Bean Validation
@Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}로케일 결정
Spring이 각 요청의 활성 로케일을 결정하는 방식을 구성해요. CookieLocaleResolver는 세션이 바뀌어도 사용자의 선택을 유지해요. LocaleChangeInterceptor를 사용하면 ?lang=de 같은 쿼리 매개변수로 로케일을 전환할 수 있어요.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import java.util.Locale;
@Configuration
public class LocaleConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
resolver.setDefaultLocale(Locale.ENGLISH);
resolver.setCookieMaxAge(3600 * 24 * 365); // 1 year
return resolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // ?lang=de switches locale
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}코드에서 번역 사용
컨트롤러에서는 MessageSource 주입으로, Thymeleaf 템플릿에서는 #{...} 구문으로 번역된 메시지에 접근해요. REST API에서는 자동으로 결정된 Locale 매개변수를 사용해요.
MessageSource를 사용하는 컨트롤러
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Locale;
@Controller
public class HomeController {
private final MessageSource messageSource;
public HomeController(MessageSource messageSource) {
this.messageSource = messageSource;
}
@GetMapping("/")
public String home(Model model, Locale locale) {
// Spring injects the resolved Locale automatically
String greeting = messageSource.getMessage(
"greeting",
new Object[]{"World"},
locale
);
model.addAttribute("greeting", greeting);
return "home";
}
}Thymeleaf 템플릿
Thymeleaf의 #{...} 표현식은 .properties 파일에서 메시지 키를 자동으로 찾아요. 매개변수는 #{key(arg0, arg1)} 구문으로 전달해요. 템플릿은 LocaleResolver가 결정한 로케일을 사용해요.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{nav.home}">Home</title>
</head>
<body>
<!-- Simple message lookup -->
<h1 th:text="#{greeting('World')}">Hello, World!</h1>
<!-- Navigation with i18n -->
<nav>
<a href="/" th:text="#{nav.home}">Home</a>
<a href="/about" th:text="#{nav.about}">About</a>
<a href="/settings" th:text="#{nav.settings}">Settings</a>
</nav>
<!-- Parameterized messages -->
<p th:text="#{cart.itemCount(3)}">3 items</p>
<!-- Language switcher -->
<div>
<a th:href="@{/(lang=en)}">English</a>
<a th:href="@{/(lang=de)}">Deutsch</a>
<a th:href="@{/(lang=ja)}">日本語</a>
</div>
<!-- Conditional text based on locale -->
<p th:if="${#locale.language == 'ja'}"
th:text="#{greeting('ユーザー')}">
こんにちは、ユーザーさん!
</p>
</body>
</html>REST API 현지화
REST API에서 Spring은 Accept-Language 헤더로부터 Locale을 자동으로 결정해요. 이를 메서드 매개변수로 주입하여 MessageSource에 전달해요. 클라이언트는 서로 다른 Accept-Language 헤더를 전송하여 언어를 전환해요.
import org.springframework.context.MessageSource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Locale;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class ApiController {
private final MessageSource messageSource;
public ApiController(MessageSource messageSource) {
this.messageSource = messageSource;
}
@GetMapping("/greeting/{name}")
public ResponseEntity<Map<String, String>> greeting(
@PathVariable String name,
Locale locale) { // Resolved from Accept-Language header
String msg = messageSource.getMessage(
"greeting", new Object[]{name}, locale
);
return ResponseEntity.ok(Map.of("message", msg));
}
// curl -H "Accept-Language: de" localhost:8080/api/greeting/Max
// → {"message": "Hallo, Max!"}
}Bean Validation 메시지
Spring은 MessageSource에서 유효성 검사 제약 조건 메시지를 자동으로 찾아요. 제약 조건 애너테이션에 {validation.name.required} 같은 중괄호 플레이스홀더를 사용하고 .properties 파일에 번역을 정의해요.
import jakarta.validation.constraints.*;
public class CreateUserRequest {
@NotBlank(message = "{validation.name.required}")
@Size(min = 2, max = 50, message = "{validation.name.size}")
private String name;
@Email(message = "{validation.email.invalid}")
private String email;
}
// In messages.properties:
// validation.name.required=Name is required
// validation.name.size=Name must be between {min} and {max} characters
// validation.email.invalid=Please enter a valid email address
//
// In messages_de.properties:
// validation.name.required=Name ist erforderlich
// validation.name.size=Name muss zwischen {min} und {max} Zeichen lang sein
// validation.email.invalid=Bitte geben Sie eine gültige E-Mail-Adresse ein복수형 및 변수 처리
Spring은 보간과 복수형 처리에 java.text.MessageFormat을 사용해요. ChoiceFormat 패턴은 기본적인 복수형 규칙을 처리하지만, ICU 복수형 전체를 지원하려면(아랍어 6가지 형식, 러시아어 3가지 형식) ICU4J 라이브러리를 추가하세요.
# MessageFormat plural syntax in messages.properties
# Uses java.text.ChoiceFormat — NOT ICU plural rules
cart.itemCount={0,choice,0#No items|1#1 item|1<{0,number} items}
# For more complex plurals, use ICU4J:
# 1. Add dependency: com.ibm.icu:icu4j
# 2. Use ICUMessageSource instead of ResourceBundleMessageSource
#
# Then you can write ICU-style plurals:
# cart.items={count, plural, one {# item} other {# items}}
# Variables with MessageFormat:
welcome.message=Welcome, {0}! You have {1,number} new {1,choice,1#notification|1<notifications}.
order.total=Order total: {0,number,currency}
event.date=Event date: {0,date,long}번역 자동화
i18n 설정을 마쳤다면 AI로 .properties 파일을 번역해요. IDE에서 AI 어시스턴트에게 원본 파일 번역을 요청하거나 CI/CD 파이프라인에서 i18n Agent CLI를 사용해요.
# Translate your .properties files with AI
# In your IDE, ask your AI assistant:
> Translate src/main/resources/messages.properties to German, Japanese, and Spanish
✓ messages_de.properties created (1.2s)
✓ messages_ja.properties created (1.5s)
✓ messages_es.properties created (1.1s)
# Or use the CLI in CI/CD:
npx i18n-agent translate src/main/resources/messages.properties --lang de,ja,es번역 품질 자동화
spring-locale-chain으로 구성 없이 시작
spring-locale-chain은 종속성 하나만으로 LocaleResolver, LocaleChangeInterceptor, 지원 로케일 유효성 검사를 자동 구성하는 오픈 소스 Spring Boot 스타터예요. application.yml에 지원할 로케일을 정의하면 나머지는 라이브러리가 처리해요.
<!-- Add spring-locale-chain for zero-config locale resolution -->
<dependency>
<groupId>io.github.i18n-agent</groupId>
<artifactId>spring-locale-chain</artifactId>
<version>1.0.0</version>
</dependency>권장 파일 구조
my-spring-app/
├── src/main/
│ ├── java/com/example/
│ │ ├── config/
│ │ │ ├── I18nConfig.java # MessageSource bean
│ │ │ └── LocaleConfig.java # LocaleResolver + interceptor
│ │ ├── controller/
│ │ │ └── HomeController.java # Uses MessageSource
│ │ └── MyApplication.java
│ └── resources/
│ ├── messages.properties # Default (English)
│ ├── messages_de.properties # German
│ ├── messages_ja.properties # Japanese
│ ├── messages_es.properties # Spanish
│ ├── application.yml # Spring config
│ └── templates/
│ └── home.html # Thymeleaf with #{...}
├── pom.xml
└── build.gradle흔히 발생하는 문제
Non-ASCII 문자가 깨져 표시됨
영어 이외의 복수형에서 작동하지 않는 ChoiceFormat
번역 변경 사항이 반영되지 않음
예기치 않은 JVM 로케일 폴백
지금 i18n Agent 사용해 보기
번역 파일을 여기에 드롭
JSON, YAML, PO, XML, CSV, Markdown, Properties
또는 클릭하여 파일 선택
대상 언어
spring-locale-chain을 활용한 로케일 폴백
pt-BR 같은 지역 로케일에 번역 키가 없으면 Spring Boot는 상위 로케일 pt를 먼저 확인하지 않고 기본 로케일로 바로 이동해요.
<!-- Maven -->
<dependency>
<groupId>ai.i18nagent</groupId>
<artifactId>spring-locale-chain</artifactId>
</dependency># application.yml
locale-chain:
fallbacks:
pt-BR:
- pt
- en
zh-Hant-HK:
- zh-Hant
- zh
- en지원되는 프레임워크 전체 목록과 75개의 기본 제공 체인은 로케일 폴백 가이드에서 확인하세요. Learn more →