Spring Boot - 国际化实现
# 1. 国际化基础概念与工作原理
国际化(Internationalization,通常缩写为i18n)指的是设计和开发应用程序时,使其能够适应不同语言和地区而无需进行工程上的修改。在Spring Boot中,国际化功能通过MessageSource
接口实现,它允许应用程序从不同的资源文件中检索本地化消息。
国际化的主要优势:
- 提升用户体验,使用户能以母语使用应用
- 扩大应用的市场覆盖范围
- 符合多国法规要求
- 无需修改代码即可支持新语言
Spring Boot国际化的核心工作原理:
- 通过
Locale
对象识别用户的语言和地区偏好 - 根据
Locale
从对应的资源文件中检索本地化消息 - 提供默认语言作为回退机制
# 2. 创建国际化资源文件
国际化资源文件是包含键值对的属性文件,用于存储不同语言的消息文本。这些文件必须遵循特定的命名约定:basename_language_country.properties
。
# 2.1 资源文件的命名规则与位置
Spring Boot默认在src/main/resources
目录下查找国际化资源文件:
src/main/resources/
├── messages.properties # 默认资源文件
├── messages_zh_CN.properties # 中文(中国)资源文件
├── messages_en_US.properties # 英文(美国)资源文件
└── messages_fr_FR.properties # 法文(法国)资源文件
2
3
4
5
命名规则说明:
messages
:基本名称(basename),可自定义zh
:语言代码(ISO 639标准)CN
:国家/地区代码(ISO 3166标准)
# 2.2 资源文件内容示例
messages.properties(默认资源文件)
# 默认语言资源文件
# 当找不到匹配用户语言的资源文件时,使用此文件中的消息
# 通用页面元素
app.title=My Application
app.welcome=Welcome to our application
app.language=Language
# 用户相关消息
user.login=Login
user.register=Register
user.email=Email Address
user.password=Password
user.username=Username
# 验证消息
validation.required=This field is required
validation.email=Please enter a valid email address
validation.password.length=Password must be at least 8 characters
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
messages_zh_CN.properties(中文资源文件)
# 中文(简体)资源文件
# 编码必须是UTF-8以正确显示中文字符
# 通用页面元素
app.title=我的应用程序
app.welcome=欢迎使用我们的应用程序
app.language=语言选择
# 用户相关消息
user.login=登录
user.register=注册
user.email=电子邮箱
user.password=密码
user.username=用户名
# 验证消息
validation.required=此字段为必填项
validation.email=请输入有效的电子邮箱地址
validation.password.length=密码长度必须至少为8个字符
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
messages_en_US.properties(美国英语资源文件)
# 英文(美国)资源文件
# 通用页面元素
app.title=My Application
app.welcome=Welcome to our application
app.language=Language
# 用户相关消息
user.login=Sign In
user.register=Sign Up
user.email=Email Address
user.password=Password
user.username=Username
# 验证消息
validation.required=This field is required
validation.email=Please enter a valid email address
validation.password.length=Password must be at least 8 characters
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.3 资源文件编码注意事项
确保所有的资源文件都使用UTF-8编码,特别是包含非ASCII字符的文件,如中文、俄文等。在大多数IDE中,可以在文件属性或保存选项中设置文件编码。
# 3. 配置Spring Boot国际化
# 3.1 基本MessageSource配置
在Spring Boot配置类中定义MessageSource
Bean,指定资源文件的位置和默认编码。
/**
* 国际化配置类
* 定义消息源和相关国际化配置
*/
@Configuration
public class InternationalizationConfig {
/**
* 配置消息源Bean
* 用于加载和解析国际化资源文件
*
* @return 配置好的消息源
*/
@Bean
public MessageSource messageSource() {
// 创建基于资源包的消息源
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 设置资源文件的基本名称(不含语言和国家后缀)
// Spring将查找classpath下的messages.properties, messages_zh_CN.properties等文件
messageSource.setBasename("messages");
// 设置资源文件的默认编码,确保正确读取非ASCII字符
messageSource.setDefaultEncoding("UTF-8");
// 设置是否使用消息格式的缓存(提高性能)
messageSource.setCacheMillis(3600000); // 1小时缓存
// 当找不到特定消息代码的翻译时是否抛出异常
// false表示返回消息代码本身而不是抛出异常
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 3.2 在application.properties中配置
Spring Boot提供了更简单的方式,可以直接在application.properties
或application.yml
中配置国际化:
# 消息源配置
spring.messages.basename=messages
spring.messages.encoding=UTF-8
spring.messages.cache-duration=3600s
spring.messages.fallback-to-system-locale=true
2
3
4
5
YAML格式:
spring:
messages:
# 指定消息资源文件的基本名称
basename: messages
# 指定资源文件的编码
encoding: UTF-8
# 设置缓存过期时间
cache-duration: 3600s
# 是否回退到系统Locale
fallback-to-system-locale: true
2
3
4
5
6
7
8
9
10
# 3.3 支持多个资源文件
如果需要将消息分散到多个资源文件中,可以指定多个基本名称:
/**
* 配置多个基本名称的消息源
*/
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 通过逗号分隔多个基本名称
// Spring将查找classpath下的messages、validation、errors等前缀的资源文件
messageSource.setBasenames("messages", "validation", "errors");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
或在属性文件中:
# 通过逗号分隔指定多个基本名称
spring.messages.basename=messages,validation,errors
2
# 4. 使用MessageSource获取本地化消息
# 4.1 在控制器中使用MessageSource
Spring Boot会自动注入配置好的MessageSource
Bean,可以在控制器或服务中使用它:
/**
* 演示如何在控制器中使用国际化消息
*/
@RestController
@RequestMapping("/api")
public class WelcomeController {
/**
* 自动注入配置好的消息源
*/
@Autowired
private MessageSource messageSource;
/**
* 获取欢迎消息的API端点
* 根据请求中的Locale返回相应语言的欢迎消息
*
* @param request HTTP请求对象,用于获取客户端的Locale
* @return 本地化的欢迎消息
*/
@GetMapping("/welcome")
public String welcome(HttpServletRequest request) {
// 从请求中获取客户端的Locale(语言和地区)
Locale locale = request.getLocale();
// 记录当前请求的Locale信息(可选)
System.out.println("Client locale: " + locale.toString());
// 使用MessageSource获取本地化消息
// 参数1: 消息代码,对应资源文件中的key
// 参数2: 消息参数,用于替换消息中的占位符(如{0}, {1}),此处不需要所以传null
// 参数3: 客户端的Locale
String welcomeMessage = messageSource.getMessage("app.welcome", null, locale);
return welcomeMessage;
}
/**
* 演示如何使用带参数的消息
*
* @param username 用户名参数
* @param request HTTP请求对象
* @return 带用户名的欢迎消息
*/
@GetMapping("/hello")
public String hello(@RequestParam String username, HttpServletRequest request) {
Locale locale = request.getLocale();
// 假设资源文件中有: user.greeting=Hello, {0}! Welcome to our application.
// 第二个参数传入对象数组,用于替换消息中的占位符
String greeting = messageSource.getMessage("user.greeting", new Object[]{username}, locale);
return greeting;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 4.2 在服务层使用MessageSource
/**
* 演示如何在服务层使用国际化消息
*/
@Service
public class UserService {
@Autowired
private MessageSource messageSource;
/**
* 生成用户注册确认消息
*
* @param user 用户对象
* @param locale 用户的语言区域
* @return 本地化的确认消息
*/
public String generateRegistrationConfirmation(User user, Locale locale) {
// 假设资源文件包含以下条目:
// registration.confirmation=Dear {0}, your registration was successful. Your ID is {1}.
// 使用用户信息作为消息参数
Object[] args = {user.getName(), user.getId()};
// 获取本地化消息
return messageSource.getMessage("registration.confirmation", args, locale);
}
/**
* 获取本地化的验证错误消息
*
* @param errorCode 错误代码
* @param locale 用户的语言区域
* @return 本地化的错误消息
*/
public String getValidationErrorMessage(String errorCode, Locale locale) {
// 尝试获取自定义错误消息,如果不存在则使用默认消息
try {
return messageSource.getMessage("validation." + errorCode, null, locale);
} catch (NoSuchMessageException e) {
// 消息代码不存在时返回通用错误消息
return messageSource.getMessage("validation.general.error", null, "Validation failed", locale);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 5. 在视图层使用国际化消息
# 5.1 在Thymeleaf模板中使用国际化
Thymeleaf与Spring Boot无缝集成,提供了简单的语法来访问国际化消息:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<!-- 使用国际化消息作为页面标题 -->
<title th:text="#{app.title}">Application Title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<!-- 使用基本的国际化消息 -->
<h1 th:text="#{app.welcome}">Welcome</h1>
<!-- 使用带参数的国际化消息 -->
<!-- 假设资源文件中有: user.greeting=Hello, {0}! -->
<p th:text="#{user.greeting(${username})}">Hello, User!</p>
<!-- 创建一个登录表单 -->
<form th:action="@{/login}" method="post">
<div>
<label th:text="#{user.email}">Email:</label>
<input type="email" name="email" required>
<small th:text="#{validation.email}">Please enter a valid email</small>
</div>
<div>
<label th:text="#{user.password}">Password:</label>
<input type="password" name="password" required>
<small th:text="#{validation.password.length}">Password must be at least 8 characters</small>
</div>
<!-- 登录按钮使用国际化文本 -->
<button type="submit" th:text="#{user.login}">Login</button>
</form>
<!-- 添加语言切换链接 -->
<div>
<span th:text="#{app.language}">Language:</span>
<a href="?lang=en_US">English</a> |
<a href="?lang=zh_CN">中文</a>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 5.2 在JSP中使用国际化
对于使用JSP的项目,需要使用JSTL标签库来访问国际化消息:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<!DOCTYPE html>
<html>
<head>
<!-- 使用spring:message标签访问国际化消息 -->
<title><spring:message code="app.title" /></title>
<meta charset="UTF-8">
</head>
<body>
<h1><spring:message code="app.welcome" /></h1>
<!-- 使用带参数的国际化消息 -->
<p><spring:message code="user.greeting" arguments="${username}" /></p>
<form action="<c:url value='/login'/>" method="post">
<div>
<label><spring:message code="user.email" /></label>
<input type="email" name="email" required>
</div>
<div>
<label><spring:message code="user.password" /></label>
<input type="password" name="password" required>
</div>
<button type="submit">
<spring:message code="user.login" />
</button>
</form>
<!-- 语言切换链接 -->
<div>
<spring:message code="app.language" />:
<a href="?lang=en_US">English</a> |
<a href="?lang=zh_CN">中文</a>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 5.3 在JavaScript代码中使用国际化
在前端JavaScript中使用国际化消息需要先将消息传递给前端:
<!-- 在Thymeleaf模板中 -->
<script th:inline="javascript">
/*<![CDATA[*/
// 创建一个全局消息对象,包含所有需要在JS中使用的国际化消息
var messages = {
welcome: /*[[#{app.welcome}]]*/ 'Welcome',
login: /*[[#{user.login}]]*/ 'Login',
required: /*[[#{validation.required}]]*/ 'This field is required',
emailInvalid: /*[[#{validation.email}]]*/ 'Invalid email'
};
// 使用这些消息
function validateForm() {
var email = document.getElementById('email').value;
if (!email) {
alert(messages.required);
return false;
}
if (!email.includes('@')) {
alert(messages.emailInvalid);
return false;
}
return true;
}
/*]]>*/
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 6. 语言切换实现方案
# 6.1 配置LocaleResolver
LocaleResolver
用于确定当前用户的Locale(语言和地区)。Spring Boot提供了几种实现:
/**
* Web国际化配置类
* 配置Locale解析器和语言切换拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 配置Locale解析器
* 决定如何确定用户的语言偏好
*
* @return 配置好的LocaleResolver
*/
@Bean
public LocaleResolver localeResolver() {
// 1. 基于Session的Locale解析器
// 将用户选择的语言存储在会话中
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
// 设置默认区域,当用户没有明确选择语言时使用
localeResolver.setDefaultLocale(Locale.US);
// 2. 基于Cookie的Locale解析器(替代方案)
/*
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
localeResolver.setCookieName("language");
localeResolver.setCookieMaxAge(3600); // Cookie有效期1小时
*/
// 3. 基于Accept-Language头的Locale解析器(替代方案)
/*
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
*/
return localeResolver;
}
/**
* 配置语言切换拦截器
* 允许通过请求参数切换语言
*
* @param registry 拦截器注册表
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 创建语言切换拦截器
LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
// 设置语言参数名,例如:?lang=zh_CN
localeInterceptor.setParamName("lang");
// 注册拦截器
registry.addInterceptor(localeInterceptor);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 6.2 实现自定义LocaleResolver
如果内置的LocaleResolver
无法满足需求,可以实现自定义的解析器:
/**
* 自定义Locale解析器
* 同时支持URL参数、Cookie和请求头来确定用户语言
*/
public class CustomLocaleResolver implements LocaleResolver {
// 语言参数名
private static final String LANGUAGE_PARAM = "lang";
// Cookie名称
private static final String LANGUAGE_COOKIE = "language";
// 默认语言
private final Locale defaultLocale = Locale.US;
/**
* 解析请求中的Locale
*
* @param request HTTP请求
* @return 解析到的Locale
*/
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 1. 首先尝试从URL参数中获取语言
String language = request.getParameter(LANGUAGE_PARAM);
if (language != null && !language.isEmpty()) {
return parseLocale(language);
}
// 2. 如果URL参数中没有语言设置,尝试从Cookie中获取
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (LANGUAGE_COOKIE.equals(cookie.getName())) {
return parseLocale(cookie.getValue());
}
}
}
// 3. 如果Cookie中也没有,使用Accept-Language头
Locale locale = request.getLocale();
return locale != null ? locale : defaultLocale;
}
/**
* 设置Locale
*
* @param request HTTP请求
* @param response HTTP响应
* @param locale 要设置的Locale
*/
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// 将选择的语言保存到Cookie中
Cookie cookie = new Cookie(LANGUAGE_COOKIE, locale.toString());
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 30); // 30天
response.addCookie(cookie);
}
/**
* 解析语言字符串为Locale对象
*
* @param language 语言字符串,例如"zh_CN"或"en"
* @return 对应的Locale对象
*/
private Locale parseLocale(String language) {
String[] parts = language.split("_");
if (parts.length == 1) {
// 只有语言代码
return new Locale(parts[0]);
} else if (parts.length == 2) {
// 语言和国家/地区代码
return new Locale(parts[0], parts[1]);
} else if (parts.length == 3) {
// 语言、国家/地区和变体
return new Locale(parts[0], parts[1], parts[2]);
}
return defaultLocale;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
注册自定义解析器:
@Bean
public LocaleResolver localeResolver() {
return new CustomLocaleResolver();
}
2
3
4
# 6.3 实现语言切换功能
在页面上添加语言切换选项:
<!-- Thymeleaf模板中的语言切换 -->
<div class="language-switcher">
<span th:text="#{app.language}">Language</span>:
<a href="?lang=en_US" th:text="English">English</a> |
<a href="?lang=zh_CN" th:text="中文">中文</a> |
<a href="?lang=fr_FR" th:text="Français">Français</a>
</div>
<!-- 或者使用下拉菜单 -->
<div class="language-dropdown">
<label for="language-select" th:text="#{app.language}">Language</label>
<select id="language-select" onchange="changeLanguage(this.value)">
<option value="en_US">English</option>
<option value="zh_CN">中文</option>
<option value="fr_FR">Français</option>
</select>
<script>
function changeLanguage(lang) {
window.location.href = '?lang=' + lang;
}
// 根据当前URL设置下拉框选中项
document.addEventListener('DOMContentLoaded', function() {
var urlParams = new URLSearchParams(window.location.search);
var currentLang = urlParams.get('lang');
if (currentLang) {
document.getElementById('language-select').value = currentLang;
}
});
</script>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 7. 高级国际化功能
# 7.1 处理复数形式和条件消息
不同语言对复数的处理方式不同,可以使用MessageFormat
来处理复数形式:
messages.properties:
cart.items={0,choice,0#Your cart is empty|1#Your cart has 1 item|1<Your cart has {0} items}
messages_zh_CN.properties:
cart.items={0,choice,0#您的购物车是空的|1#您的购物车有1件商品|1<您的购物车有{0}件商品}
使用示例:
@GetMapping("/cart")
public String getCartMessage(@RequestParam(defaultValue = "0") int itemCount, HttpServletRequest request) {
Locale locale = request.getLocale();
return messageSource.getMessage("cart.items", new Object[]{itemCount}, locale);
}
2
3
4
5
# 7.2 时间和日期的国际化
时间和日期格式在不同国家也有差异,可以使用Java的DateTimeFormatter
结合Locale
进行格式化:
/**
* 日期时间国际化示例
*/
@GetMapping("/datetime")
public Map<String, String> getLocalizedDateTime(HttpServletRequest request) {
Locale locale = request.getLocale();
ZonedDateTime now = ZonedDateTime.now();
// 使用本地化的格式化器
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM).withLocale(locale);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(locale);
// 创建结果Map
Map<String, String> result = new HashMap<>();
result.put("locale", locale.toString());
result.put("date", now.format(dateFormatter));
result.put("time", now.format(timeFormatter));
result.put("dateTime", now.format(dateTimeFormatter));
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 7.3 货币和数字的国际化
不同国家/地区使用不同的货币符号和数字格式:
/**
* 货币和数字国际化示例
*/
@GetMapping("/format")
public Map<String, String> getLocalizedFormats(HttpServletRequest request) {
Locale locale = request.getLocale();
double amount = 1234567.89;
// 获取本地化的格式化器
NumberFormat numberFormat = NumberFormat.getNumberInstance(locale);
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
NumberFormat percentFormat = NumberFormat.getPercentInstance(locale);
// 创建结果Map
Map<String, String> result = new HashMap<>();
result.put("locale", locale.toString());
result.put("number", numberFormat.format(amount));
result.put("currency", currencyFormat.format(amount));
result.put("percent", percentFormat.format(amount / 100));
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22