国际化(Internationalization,通常缩写为i18n,因为在"i"和"n"之间有18个字母)是指设计和开发软件时,使其能够适应不同语言和地区而无需进行工程上的更改。通过国际化,同一个应用程序可以在不同国家、使用不同语言的环境中运行,并为用户提供本地化的界面和内容。
Spring Boot提供了多种区域解析器:
AcceptHeaderLocaleResolver:
CookieLocaleResolver:
SessionLocaleResolver:
FixedLocaleResolver:
Spring Boot默认包含了国际化支持,无需额外依赖。如果使用Thymeleaf模板引擎,需要添加:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
在src/main/resources
目录下创建以下文件:
messages.properties(默认,当找不到特定语言的资源文件时使用):
greeting=你好
welcome.message=欢迎来到Spring Boot
language.change=更改语言
messages_en_US.properties(英文-美国):
greeting=Hello
welcome.message=Welcome to Spring Boot
language.change=Change Language
messages_zh_CN.properties(中文-中国):
greeting=你好
welcome.message=欢迎来到Spring Boot
language.change=更改语言
messages_ja_JP.properties(日语-日本):
greeting=こんにちは
welcome.message=Spring Bootへようこそ
language.change=言語を変更する
在application.properties
或application.yml
中配置:
spring:
messages:
basename: messages # 资源文件基础名(不包括语言和地区后缀)
encoding: UTF-8 # 资源文件编码
cache-duration: 3600 # 缓存时间(秒)
fallback-to-system-locale: false # 当找不到对应Locale的资源文件时,是否回退到系统Locale
创建配置类:
@Configuration
public class InternationalizationConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 设置默认语言为简体中文
return resolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // 设置切换语言的参数名
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
创建一个简单的Thymeleaf模板(index.html
):
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot i18ntitle>
head>
<body>
<h1 th:text="#{greeting}">默认问候语h1>
<p th:text="#{welcome.message}">默认欢迎消息p>
<div>
<a href="?lang=zh_CN" th:text="中文">中文a>
<a href="?lang=en_US" th:text="English">Englisha>
<a href="?lang=ja_JP" th:text="日本語">日本語a>
div>
body>
html>
创建一个控制器:
@Controller
public class HomeController {
@Autowired
private MessageSource messageSource;
@GetMapping("/")
public String home() {
return "index"; // 返回index.html模板
}
@GetMapping("/greeting-api")
@ResponseBody
public String getGreeting(Locale locale) {
// 在代码中获取国际化消息
return messageSource.getMessage("greeting", null, locale);
}
@GetMapping("/welcome-api")
@ResponseBody
public String getWelcome(Locale locale) {
// 使用参数化消息
return messageSource.getMessage("welcome.user",
new Object[]{"John"},
locale);
}
}
为参数化消息,添加以下内容到资源文件:
messages.properties:
welcome.user=欢迎,{0}!
messages_en_US.properties:
welcome.user=Welcome, {0}!
创建一个REST控制器:
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private MessageSource messageSource;
@GetMapping("/messages")
public Map<String, String> getMessages(
@RequestHeader(name = "Accept-Language", required = false) Locale locale) {
if (locale == null) {
locale = Locale.getDefault();
}
Map<String, String> messages = new HashMap<>();
messages.put("greeting", messageSource.getMessage("greeting", null, locale));
messages.put("welcome", messageSource.getMessage("welcome.message", null, locale));
return messages;
}
}
资源文件中可以包含参数占位符:
messages.properties:
user.greeting=你好,{0}!今天是{1}。
items.count=你有{0}个物品在购物车中。
messages_en_US.properties:
user.greeting=Hello, {0}! Today is {1}.
items.count=You have {0} items in your cart.
在Java代码中使用:
@GetMapping("/parameterized")
@ResponseBody
public String getParameterizedMessage(Locale locale) {
// 格式化当前日期
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
// 使用多个参数
return messageSource.getMessage("user.greeting",
new Object[]{"Alice", today},
locale);
}
资源文件中可以定义不同数量的消息形式:
messages.properties:
cart.items=购物车中有{0}个商品
cart.items.zero=购物车是空的
cart.items.one=购物车中有1个商品
cart.items.many=购物车中有{0}个商品
messages_en_US.properties:
cart.items=There are {0} items in the cart
cart.items.zero=The cart is empty
cart.items.one=There is 1 item in the cart
cart.items.many=There are {0} items in the cart
在Java代码中使用:
@GetMapping("/cart/{count}")
@ResponseBody
public String getCartMessage(@PathVariable int count, Locale locale) {
String key;
if (count == 0) {
key = "cart.items.zero";
} else if (count == 1) {
key = "cart.items.one";
} else {
key = "cart.items.many";
}
return messageSource.getMessage(key, new Object[]{count}, locale);
}
创建一个自定义的区域解析器,例如基于用户设置:
public class UserPreferenceLocaleResolver implements LocaleResolver {
@Autowired
private UserService userService; // 假设有一个用户服务
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取当前用户
User user = userService.getCurrentUser();
if (user != null && user.getPreferredLocale() != null) {
// 使用用户首选语言
return user.getPreferredLocale();
}
// 回退到请求头中的语言
String acceptLanguage = request.getHeader("Accept-Language");
if (StringUtils.hasText(acceptLanguage)) {
return Locale.forLanguageTag(acceptLanguage.split(",")[0]);
}
// 默认语言
return Locale.SIMPLIFIED_CHINESE;
}
@Override
public void setLocale(HttpServletRequest request,
HttpServletResponse response,
Locale locale) {
// 更新用户首选语言
User user = userService.getCurrentUser();
if (user != null) {
user.setPreferredLocale(locale);
userService.updateUser(user);
}
}
}
注册自定义解析器:
@Bean
public LocaleResolver localeResolver() {
return new UserPreferenceLocaleResolver();
}
对于大型应用,可以按模块或功能区域组织资源文件:
resources/
├── i18n/
│ ├── common/
│ │ ├── messages.properties
│ │ ├── messages_en_US.properties
│ │ └── messages_zh_CN.properties
│ ├── validation/
│ │ ├── messages.properties
│ │ ├── messages_en_US.properties
│ │ └── messages_zh_CN.properties
│ └── user/
│ ├── messages.properties
│ ├── messages_en_US.properties
│ └── messages_zh_CN.properties
配置多个资源包:
spring:
messages:
basename: i18n/common/messages,i18n/validation/messages,i18n/user/messages
采用一致的命名约定:
module.submodule.element
user.profile.title
error.validation.email
, error.validation.password
示例:
# 用户模块
user.profile.title=用户资料
user.profile.name=姓名
user.profile.email=电子邮件
# 错误消息
error.validation.required=此字段为必填项
error.validation.email=请输入有效的电子邮件地址
配置默认消息,当找不到对应的翻译时使用:
@GetMapping("/safe-message")
@ResponseBody
public String getSafeMessage(Locale locale) {
// 提供默认消息
return messageSource.getMessage(
"unknown.key",
null,
"This is a default message when translation is missing",
locale
);
}
或者在配置中设置使用消息代码作为默认消息:
spring:
messages:
use-code-as-default-message: true
在开发环境中,可以配置资源文件的自动重载,无需重启应用:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheMillis(1000); // 缓存时间1秒,便于开发时测试
return messageSource;
}
当应用较大时,可以将资源文件分成多个包:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(
"classpath:messages/core",
"classpath:messages/validation",
"classpath:messages/ui"
);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
对于需要在数据库中存储的多语言内容,可以创建专门的实体和表:
@Entity
public class TranslatedContent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String contentKey; // 内容的唯一标识
@Column(nullable = false)
private String locale; // 语言代码
@Column(nullable = false, length = 4000)
private String content; // 翻译内容
// getters and setters
}
创建一个服务来获取翻译:
@Service
public class DatabaseMessageService {
@Autowired
private TranslatedContentRepository repository;
public String getMessage(String key, Locale locale) {
TranslatedContent content = repository.findByContentKeyAndLocale(
key, locale.toString());
if (content != null) {
return content.getContent();
}
// 回退到默认语言
content = repository.findByContentKeyAndLocale(key, "en_US");
return content != null ? content.getContent() : "???" + key + "???";
}
}
创建测试类来验证国际化配置:
@SpringBootTest
public class InternationalizationTest {
@Autowired
private MessageSource messageSource;
@Test
public void testDefaultMessages() {
String greeting = messageSource.getMessage("greeting", null, Locale.SIMPLIFIED_CHINESE);
assertEquals("你好", greeting);
}
@Test
public void testEnglishMessages() {
String greeting = messageSource.getMessage("greeting", null, Locale.US);
assertEquals("Hello", greeting);
}
@Test
public void testParameterizedMessages() {
String message = messageSource.getMessage(
"welcome.user",
new Object[]{"John"},
Locale.US
);
assertEquals("Welcome, John!", message);
}
@Test
public void testFallbackMessages() {
// 测试不存在的键是否正确回退到默认消息
String message = messageSource.getMessage(
"nonexistent.key",
null,
"Default Message",
Locale.US
);
assertEquals("Default Message", message);
}
}
测试控制器是否正确处理国际化:
@SpringBootTest
@AutoConfigureMockMvc
public class InternationalizationControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testDefaultLocale() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("欢迎来到Spring Boot")));
}
@Test
public void testEnglishLocale() throws Exception {
mockMvc.perform(
get("/")
.header("Accept-Language", "en-US")
)
.andExpect(status().isOk())
.andExpect(content().string(containsString("Welcome to Spring Boot")));
}
@Test
public void testLocaleChangeParameter() throws Exception {
mockMvc.perform(get("/?lang=en_US"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Welcome to Spring Boot")));
}
}
问题:资源文件中的非ASCII字符显示为乱码。
解决方案:
spring:
messages:
encoding: UTF-8
问题:应用无法找到资源文件。
解决方案:
messages_zh_CN.properties
)src/main/resources
)spring:
messages:
basename: messages # 不要包含.properties后缀
问题:点击语言切换链接后,语言没有变化。
解决方案:
LocaleChangeInterceptor
并添加到拦截器注册表locale
,但上面示例中使用了lang
)LocaleResolver
配置正确@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // 确保与URL参数一致
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
问题:应用启动时使用了错误的默认语言。
解决方案:
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 设置默认语言
return resolver;
}
spring:
messages:
fallback-to-system-locale: false
messages.properties:
login.title=登录系统
login.username=用户名
login.password=密码
login.remember=记住我
login.button=登录
login.error=用户名或密码错误
login.success=登录成功
login.language=语言
messages_en_US.properties:
login.title=Login System
login.username=Username
login.password=Password
login.remember=Remember Me
login.button=Sign In
login.error=Invalid username or password
login.success=Login successful
login.language=Language
@Controller
public class LoginController {
@Autowired
private MessageSource messageSource;
@GetMapping("/login")
public String loginPage() {
return "login";
}
@PostMapping("/login")
public String processLogin(
@RequestParam String username,
@RequestParam String password,
RedirectAttributes redirectAttributes,
Locale locale) {
// 简单的登录逻辑示例
if ("admin".equals(username) && "password".equals(password)) {
redirectAttributes.addFlashAttribute(
"message",
messageSource.getMessage("login.success", null, locale)
);
return "redirect:/dashboard";
} else {
redirectAttributes.addFlashAttribute(
"error",
messageSource.getMessage("login.error", null, locale)
);
return "redirect:/login";
}
}
@GetMapping("/dashboard")
public String dashboard() {
return "dashboard";
}
}
login.html:
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{login.title}">Logintitle>
<style>
body {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.error {
color: red;
margin-bottom: 15px;
}
.success {
color: green;
margin-bottom: 15px;
}
.language-selector {
margin-top: 20px;
}
style>
head>
<body>
<h1 th:text="#{login.title}">Login Systemh1>
<div class="error" th:if="${error}" th:text="${error}">div>
<div class="success" th:if="${message}" th:text="${message}">div>
<form method="post" th:action="@{/login}">
<div class="form-group">
<label for="username" th:text="#{login.username}">Usernamelabel>
<input type="text" id="username" name="username" required>
div>
<div class="form-group">
<label for="password" th:text="#{login.password}">Passwordlabel>
<input type="password" id="password" name="password" required>
div>
<div class="form-group">
<label>
<input type="checkbox" name="remember">
<span th:text="#{login.remember}">Remember Mespan>
label>
div>
<div class="form-group">
<button type="submit" th:text="#{login.button}">Sign Inbutton>
div>
form>
<div class="language-selector">
<span th:text="#{login.language}">Languagespan>:
<a href="?lang=zh_CN">中文a> |
<a href="?lang=en_US">Englisha>
div>
body>
html>
dashboard.html:
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Dashboardtitle>
head>
<body>
<h1>Dashboardh1>
<div class="success" th:if="${message}" th:text="${message}">div>
<p>You are logged in!p>
<div class="language-selector">
<span th:text="#{login.language}">Languagespan>:
<a href="?lang=zh_CN">中文a> |
<a href="?lang=en_US">Englisha>
div>
body>
html>
Spring Boot提供了强大而灵活的国际化支持,通过本指南我们学习了:
通过合理使用Spring Boot的国际化功能,可以轻松构建多语言应用,提升全球用户的体验。关键是理解核心组件(如MessageSource和LocaleResolver)的工作原理,并采用一致的资源文件组织和命名约定。